mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-10 07:08:05 -05:00
drk: added fee call to transfer
This commit is contained in:
@@ -23,7 +23,7 @@ use darkfi::rpc::jsonrpc::{ErrorCode::ServerError, JsonError, JsonResult};
|
||||
pub enum RpcError {
|
||||
// Transaction-related errors
|
||||
TxSimulationFail = -32110,
|
||||
TxBroadcastFail = -32111,
|
||||
TxGasCalculationFail = -32111,
|
||||
|
||||
// State-related errors,
|
||||
NotSynced = -32120,
|
||||
@@ -43,7 +43,7 @@ fn to_tuple(e: RpcError) -> (i32, String) {
|
||||
let msg = match e {
|
||||
// Transaction-related errors
|
||||
RpcError::TxSimulationFail => "Failed simulating transaction state change",
|
||||
RpcError::TxBroadcastFail => "Failed broadcasting transaction",
|
||||
RpcError::TxGasCalculationFail => "Failed to calculate transaction's gas",
|
||||
// State-related errors
|
||||
RpcError::NotSynced => "Blockchain is not synced",
|
||||
RpcError::UnknownBlockHeight => "Did not find block height",
|
||||
|
||||
@@ -73,6 +73,7 @@ impl RequestHandler for Darkfid {
|
||||
"tx.broadcast" => self.tx_broadcast(req.id, req.params).await,
|
||||
"tx.pending" => self.tx_pending(req.id, req.params).await,
|
||||
"tx.clean_pending" => self.tx_pending(req.id, req.params).await,
|
||||
"tx.calculate_gas" => self.tx_calculate_gas(req.id, req.params).await,
|
||||
|
||||
// ==============
|
||||
// Invalid method
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
use darkfi_serial::deserialize_async;
|
||||
use log::error;
|
||||
use log::{error, warn};
|
||||
use tinyjson::JsonValue;
|
||||
|
||||
use darkfi::{
|
||||
@@ -135,8 +135,7 @@ impl Darkfid {
|
||||
|
||||
self.p2p.broadcast(&tx).await;
|
||||
if self.p2p.hosts().channels().await.is_empty() {
|
||||
error!(target: "darkfid::rpc::tx_broadcast", "Failed broadcasting tx, no connected channels");
|
||||
return server_error(RpcError::TxBroadcastFail, id, None)
|
||||
warn!(target: "darkfid::rpc::tx_broadcast", "No connected channels to broadcast tx");
|
||||
}
|
||||
|
||||
let tx_hash = tx.hash().to_string();
|
||||
@@ -209,4 +208,56 @@ impl Darkfid {
|
||||
|
||||
JsonResponse::new(JsonValue::Array(pending_txs), id).into()
|
||||
}
|
||||
|
||||
// RPCAPI:
|
||||
// Compute provided transaction's total gas, against current best fork.
|
||||
// Returns the gas value if the transaction is valid, otherwise, a corresponding
|
||||
// error.
|
||||
//
|
||||
// --> {"jsonrpc": "2.0", "method": "tx.calculate_gas", "params": ["base64encodedTX", "include_fee"], "id": 1}
|
||||
// <-- {"jsonrpc": "2.0", "result": true, "id": 1}
|
||||
pub async fn tx_calculate_gas(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||
if params.len() != 2 || !params[0].is_string() || !params[1].is_bool() {
|
||||
return JsonError::new(InvalidParams, None, id).into()
|
||||
}
|
||||
|
||||
if !*self.validator.synced.read().await {
|
||||
error!(target: "darkfid::rpc::tx_calculate_gas", "Blockchain is not synced");
|
||||
return server_error(RpcError::NotSynced, id, None)
|
||||
}
|
||||
|
||||
// Try to deserialize the transaction
|
||||
let tx_enc = params[0].get::<String>().unwrap().trim();
|
||||
let tx_bytes = match base64::decode(tx_enc) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
error!(target: "darkfid::rpc::tx_calculate_gas", "Failed decoding base64 transaction");
|
||||
return server_error(RpcError::ParseError, id, None)
|
||||
}
|
||||
};
|
||||
|
||||
let tx: Transaction = match deserialize_async(&tx_bytes).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!(target: "darkfid::rpc::tx_calculate_gas", "Failed deserializing bytes into Transaction: {}", e);
|
||||
return server_error(RpcError::ParseError, id, None)
|
||||
}
|
||||
};
|
||||
|
||||
// Parse the include fee flag
|
||||
let include_fee = params[1].get::<bool>().unwrap();
|
||||
|
||||
// Simulate state transition
|
||||
let result = self.validator.calculate_gas(&tx, *include_fee).await;
|
||||
if result.is_err() {
|
||||
error!(
|
||||
target: "darkfid::rpc::tx_calculate_gas", "Failed to validate state transition: {}",
|
||||
result.err().unwrap()
|
||||
);
|
||||
return server_error(RpcError::TxGasCalculationFail, id, None)
|
||||
};
|
||||
|
||||
JsonResponse::new(JsonValue::Number(result.unwrap() as f64), id).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
-- Wallet definitions for this contract.
|
||||
-- We store data that is needed to be able to receive and send tokens.
|
||||
-- TODO: The tables should be prefixed with ContractId to prevent collision
|
||||
|
||||
-- Arbitrary info that is potentially useful
|
||||
CREATE TABLE IF NOT EXISTS BZHKGQ26bzmBithTQYTJtjo2QdCqpkR9tjSBopT4yf4o_money_info (
|
||||
|
||||
@@ -22,12 +22,22 @@ use lazy_static::lazy_static;
|
||||
use rand::rngs::OsRng;
|
||||
use rusqlite::types::Value;
|
||||
|
||||
use darkfi::{zk::halo2::Field, Error, Result};
|
||||
use darkfi::{
|
||||
tx::Transaction,
|
||||
zk::{halo2::Field, proof::ProvingKey, Proof},
|
||||
zkas::ZkBinary,
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_money_contract::{
|
||||
client::{MoneyNote, OwnCoin},
|
||||
client::{
|
||||
compute_remainder_blind,
|
||||
fee_v1::{create_fee_proof, FeeCallInput, FeeCallOutput, FEE_CALL_GAS},
|
||||
MoneyNote, OwnCoin,
|
||||
},
|
||||
model::{
|
||||
Coin, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenFreezeParamsV1,
|
||||
MoneyTokenMintParamsV1, MoneyTransferParamsV1, Nullifier, TokenId, DARK_TOKEN_ID,
|
||||
Coin, Input, MoneyFeeParamsV1, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1,
|
||||
MoneyTokenFreezeParamsV1, MoneyTokenMintParamsV1, MoneyTransferParamsV1, Nullifier, Output,
|
||||
TokenId, DARK_TOKEN_ID,
|
||||
},
|
||||
MoneyFunction,
|
||||
};
|
||||
@@ -38,8 +48,9 @@ use darkfi_sdk::{
|
||||
ScalarBlind, SecretKey, MONEY_CONTRACT_ID,
|
||||
},
|
||||
pasta::pallas,
|
||||
ContractCall,
|
||||
};
|
||||
use darkfi_serial::{deserialize_async, serialize_async};
|
||||
use darkfi_serial::{deserialize_async, serialize_async, AsyncEncodable};
|
||||
|
||||
use crate::{
|
||||
convert_named_params,
|
||||
@@ -401,95 +412,124 @@ impl Drk {
|
||||
};
|
||||
|
||||
let mut owncoins = Vec::with_capacity(rows.len());
|
||||
|
||||
for row in rows {
|
||||
let Value::Blob(ref coin_bytes) = row[0] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Coin bytes parsing failed"))
|
||||
};
|
||||
let coin: Coin = deserialize_async(coin_bytes).await?;
|
||||
|
||||
let Value::Integer(is_spent) = row[1] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Is spent parsing failed"))
|
||||
};
|
||||
let Ok(is_spent) = u64::try_from(is_spent) else {
|
||||
return Err(Error::ParseFailed("[get_coins] Is spent parsing failed"))
|
||||
};
|
||||
let is_spent = is_spent > 0;
|
||||
|
||||
let Value::Blob(ref value_bytes) = row[2] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Value bytes parsing failed"))
|
||||
};
|
||||
let value: u64 = deserialize_async(value_bytes).await?;
|
||||
|
||||
let Value::Blob(ref token_id_bytes) = row[3] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Token ID bytes parsing failed"))
|
||||
};
|
||||
let token_id: TokenId = deserialize_async(token_id_bytes).await?;
|
||||
|
||||
let Value::Blob(ref spend_hook_bytes) = row[4] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Spend hook bytes parsing failed"))
|
||||
};
|
||||
let spend_hook: pallas::Base = deserialize_async(spend_hook_bytes).await?;
|
||||
|
||||
let Value::Blob(ref user_data_bytes) = row[5] else {
|
||||
return Err(Error::ParseFailed("[get_coins] User data bytes parsing failed"))
|
||||
};
|
||||
let user_data: pallas::Base = deserialize_async(user_data_bytes).await?;
|
||||
|
||||
let Value::Blob(ref coin_blind_bytes) = row[6] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Coin blind bytes parsing failed"))
|
||||
};
|
||||
let coin_blind: BaseBlind = deserialize_async(coin_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref value_blind_bytes) = row[7] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Value blind bytes parsing failed"))
|
||||
};
|
||||
let value_blind: ScalarBlind = deserialize_async(value_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref token_blind_bytes) = row[8] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Token blind bytes parsing failed"))
|
||||
};
|
||||
let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref secret_bytes) = row[9] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Secret bytes parsing failed"))
|
||||
};
|
||||
let secret: SecretKey = deserialize_async(secret_bytes).await?;
|
||||
|
||||
// TODO: Remove from SQL store, can be derived ondemand
|
||||
let Value::Blob(ref nullifier_bytes) = row[10] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Nullifier bytes parsing failed"))
|
||||
};
|
||||
let _nullifier: Nullifier = deserialize_async(nullifier_bytes).await?;
|
||||
|
||||
let Value::Blob(ref leaf_position_bytes) = row[11] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Leaf position bytes parsing failed"))
|
||||
};
|
||||
let leaf_position: bridgetree::Position =
|
||||
deserialize_async(leaf_position_bytes).await?;
|
||||
|
||||
let Value::Blob(ref memo) = row[12] else {
|
||||
return Err(Error::ParseFailed("[get_coins] Memo parsing failed"))
|
||||
};
|
||||
|
||||
let note = MoneyNote {
|
||||
value,
|
||||
token_id,
|
||||
spend_hook: spend_hook.into(),
|
||||
user_data,
|
||||
coin_blind,
|
||||
value_blind,
|
||||
token_blind,
|
||||
memo: memo.clone(),
|
||||
};
|
||||
let owncoin = OwnCoin { coin, note, secret, leaf_position };
|
||||
|
||||
owncoins.push((owncoin, is_spent))
|
||||
owncoins.push(self.parse_coin_record(&row).await?)
|
||||
}
|
||||
|
||||
Ok(owncoins)
|
||||
}
|
||||
|
||||
/// Fetch provided token unspend balances from the wallet.
|
||||
pub async fn get_token_coins(&self, token_id: &TokenId) -> Result<Vec<OwnCoin>> {
|
||||
let query = self.wallet.query_multiple(
|
||||
&MONEY_COINS_TABLE,
|
||||
&[],
|
||||
convert_named_params! {(MONEY_COINS_COL_IS_SPENT, false), (MONEY_TOKENS_COL_TOKEN_ID, serialize_async(token_id).await), (MONEY_COINS_COL_SPEND_HOOK, serialize_async(&FuncId::none()).await)},
|
||||
)
|
||||
.await;
|
||||
|
||||
let rows = match query {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return Err(Error::RusqliteError(format!(
|
||||
"[get_coins] Coins retrieval failed: {e:?}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut owncoins = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
owncoins.push(self.parse_coin_record(&row).await?.0)
|
||||
}
|
||||
|
||||
Ok(owncoins)
|
||||
}
|
||||
|
||||
/// Auxiliary function to parse a `MONEY_COINS_TABLE` record.
|
||||
/// The boolean in the returned tuple notes if the coin was marked as spent.
|
||||
async fn parse_coin_record(&self, row: &[Value]) -> Result<(OwnCoin, bool)> {
|
||||
let Value::Blob(ref coin_bytes) = row[0] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Coin bytes parsing failed"))
|
||||
};
|
||||
let coin: Coin = deserialize_async(coin_bytes).await?;
|
||||
|
||||
let Value::Integer(is_spent) = row[1] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed"))
|
||||
};
|
||||
let Ok(is_spent) = u64::try_from(is_spent) else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Is spent parsing failed"))
|
||||
};
|
||||
let is_spent = is_spent > 0;
|
||||
|
||||
let Value::Blob(ref value_bytes) = row[2] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Value bytes parsing failed"))
|
||||
};
|
||||
let value: u64 = deserialize_async(value_bytes).await?;
|
||||
|
||||
let Value::Blob(ref token_id_bytes) = row[3] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Token ID bytes parsing failed"))
|
||||
};
|
||||
let token_id: TokenId = deserialize_async(token_id_bytes).await?;
|
||||
|
||||
let Value::Blob(ref spend_hook_bytes) = row[4] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Spend hook bytes parsing failed"))
|
||||
};
|
||||
let spend_hook: pallas::Base = deserialize_async(spend_hook_bytes).await?;
|
||||
|
||||
let Value::Blob(ref user_data_bytes) = row[5] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] User data bytes parsing failed"))
|
||||
};
|
||||
let user_data: pallas::Base = deserialize_async(user_data_bytes).await?;
|
||||
|
||||
let Value::Blob(ref coin_blind_bytes) = row[6] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Coin blind bytes parsing failed"))
|
||||
};
|
||||
let coin_blind: BaseBlind = deserialize_async(coin_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref value_blind_bytes) = row[7] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Value blind bytes parsing failed"))
|
||||
};
|
||||
let value_blind: ScalarBlind = deserialize_async(value_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref token_blind_bytes) = row[8] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Token blind bytes parsing failed"))
|
||||
};
|
||||
let token_blind: BaseBlind = deserialize_async(token_blind_bytes).await?;
|
||||
|
||||
let Value::Blob(ref secret_bytes) = row[9] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Secret bytes parsing failed"))
|
||||
};
|
||||
let secret: SecretKey = deserialize_async(secret_bytes).await?;
|
||||
|
||||
// TODO: Remove from SQL store, can be derived ondemand
|
||||
let Value::Blob(ref nullifier_bytes) = row[10] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Nullifier bytes parsing failed"))
|
||||
};
|
||||
let _nullifier: Nullifier = deserialize_async(nullifier_bytes).await?;
|
||||
|
||||
let Value::Blob(ref leaf_position_bytes) = row[11] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Leaf position bytes parsing failed"))
|
||||
};
|
||||
let leaf_position: bridgetree::Position = deserialize_async(leaf_position_bytes).await?;
|
||||
|
||||
let Value::Blob(ref memo) = row[12] else {
|
||||
return Err(Error::ParseFailed("[parse_coin_record] Memo parsing failed"))
|
||||
};
|
||||
|
||||
let note = MoneyNote {
|
||||
value,
|
||||
token_id,
|
||||
spend_hook: spend_hook.into(),
|
||||
user_data,
|
||||
coin_blind,
|
||||
value_blind,
|
||||
token_blind,
|
||||
memo: memo.clone(),
|
||||
};
|
||||
|
||||
Ok((OwnCoin { coin, note, secret, leaf_position }, is_spent))
|
||||
}
|
||||
|
||||
/// Create an alias record for provided Token ID.
|
||||
pub async fn add_alias(&self, alias: String, token_id: TokenId) -> WalletDbResult<()> {
|
||||
println!("Generating alias {alias} for Token: {token_id}");
|
||||
@@ -642,7 +682,10 @@ impl Drk {
|
||||
match MoneyFunction::try_from(data[0])? {
|
||||
MoneyFunction::FeeV1 => {
|
||||
println!("[apply_tx_money_data] Found Money::FeeV1 call");
|
||||
// TODO: implement
|
||||
let params: MoneyFeeParamsV1 = deserialize_async(&data[9..]).await?;
|
||||
nullifiers.push(params.input.nullifier);
|
||||
coins.push(params.output.coin);
|
||||
notes.push(params.output.note);
|
||||
}
|
||||
MoneyFunction::GenesisMintV1 => {
|
||||
println!("[apply_tx_money_data] Found Money::GenesisMintV1 call");
|
||||
@@ -871,4 +914,118 @@ impl Drk {
|
||||
// Else parse input
|
||||
Ok(TokenId::from_str(input.as_str())?)
|
||||
}
|
||||
|
||||
/// Create and append a `Money::Fee` call to a given [`Transaction`].
|
||||
///
|
||||
/// Optionally takes a set of spent coins in order not to reuse them here.
|
||||
///
|
||||
/// Returns the `Fee` call, and all necessary data and parameters related.
|
||||
pub async fn append_fee_call(
|
||||
&self,
|
||||
tx: &Transaction,
|
||||
public_key: PublicKey,
|
||||
money_merkle_tree: &MerkleTree,
|
||||
fee_pk: &ProvingKey,
|
||||
fee_zkbin: &ZkBinary,
|
||||
spent_coins: Option<&[OwnCoin]>,
|
||||
) -> Result<(ContractCall, Vec<Proof>, Vec<SecretKey>)> {
|
||||
// First we verify the fee-less transaction to see how much gas it uses for execution
|
||||
// and verification.
|
||||
let gas_used = FEE_CALL_GAS + self.get_tx_gas(tx, false).await?;
|
||||
|
||||
// Knowing the total gas, we can now find an OwnCoin of enough value
|
||||
// so that we can create a valid Money::Fee call.
|
||||
let mut available_coins = self.get_token_coins(&DARK_TOKEN_ID).await?;
|
||||
available_coins.retain(|x| x.note.value > gas_used);
|
||||
if let Some(spent_coins) = spent_coins {
|
||||
available_coins.retain(|x| !spent_coins.contains(x));
|
||||
}
|
||||
if available_coins.is_empty() {
|
||||
return Err(Error::Custom("Not enough native tokens to pay for fees".to_string()))
|
||||
}
|
||||
|
||||
let coin = &available_coins[0];
|
||||
let change_value = coin.note.value - gas_used;
|
||||
|
||||
// Input and output setup
|
||||
let input = FeeCallInput {
|
||||
coin: coin.clone(),
|
||||
merkle_path: money_merkle_tree.witness(coin.leaf_position, 0).unwrap(),
|
||||
user_data_blind: BaseBlind::random(&mut OsRng),
|
||||
};
|
||||
|
||||
let output = FeeCallOutput {
|
||||
public_key,
|
||||
value: change_value,
|
||||
token_id: coin.note.token_id,
|
||||
blind: BaseBlind::random(&mut OsRng),
|
||||
spend_hook: FuncId::none(),
|
||||
user_data: pallas::Base::ZERO,
|
||||
};
|
||||
|
||||
// Create blinding factors
|
||||
let token_blind = BaseBlind::random(&mut OsRng);
|
||||
let input_value_blind = ScalarBlind::random(&mut OsRng);
|
||||
let fee_value_blind = ScalarBlind::random(&mut OsRng);
|
||||
let output_value_blind = compute_remainder_blind(&[input_value_blind], &[fee_value_blind]);
|
||||
|
||||
// Create an ephemeral signing key
|
||||
let signature_secret = SecretKey::random(&mut OsRng);
|
||||
|
||||
// Create the actual fee proof
|
||||
let (proof, public_inputs) = create_fee_proof(
|
||||
fee_zkbin,
|
||||
fee_pk,
|
||||
&input,
|
||||
input_value_blind,
|
||||
&output,
|
||||
output_value_blind,
|
||||
output.spend_hook,
|
||||
output.user_data,
|
||||
output.blind,
|
||||
token_blind,
|
||||
signature_secret,
|
||||
)?;
|
||||
|
||||
// Encrypted note for the output
|
||||
let note = MoneyNote {
|
||||
coin_blind: output.blind,
|
||||
value: output.value,
|
||||
token_id: output.token_id,
|
||||
spend_hook: output.spend_hook,
|
||||
user_data: output.user_data,
|
||||
value_blind: output_value_blind,
|
||||
token_blind,
|
||||
memo: vec![],
|
||||
};
|
||||
|
||||
let encrypted_note = AeadEncryptedNote::encrypt(¬e, &output.public_key, &mut OsRng)?;
|
||||
|
||||
let params = MoneyFeeParamsV1 {
|
||||
input: Input {
|
||||
value_commit: public_inputs.input_value_commit,
|
||||
token_commit: public_inputs.token_commit,
|
||||
nullifier: public_inputs.nullifier,
|
||||
merkle_root: public_inputs.merkle_root,
|
||||
user_data_enc: public_inputs.input_user_data_enc,
|
||||
signature_public: public_inputs.signature_public,
|
||||
},
|
||||
output: Output {
|
||||
value_commit: public_inputs.output_value_commit,
|
||||
token_commit: public_inputs.token_commit,
|
||||
coin: public_inputs.output_coin,
|
||||
note: encrypted_note,
|
||||
},
|
||||
fee_value_blind,
|
||||
token_blind,
|
||||
};
|
||||
|
||||
// Encode the contract call
|
||||
let mut data = vec![MoneyFunction::FeeV1 as u8];
|
||||
gas_used.encode_async(&mut data).await?;
|
||||
params.encode_async(&mut data).await?;
|
||||
let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
|
||||
|
||||
Ok((call, vec![proof], vec![signature_secret]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ impl Drk {
|
||||
}
|
||||
}
|
||||
|
||||
// Queries darkfid for a block with given height
|
||||
// Queries darkfid for a block with given height.
|
||||
async fn get_block_by_height(&self, height: u32) -> Result<BlockInfo> {
|
||||
let req = JsonRequest::new(
|
||||
"blockchain.get_block",
|
||||
@@ -293,7 +293,7 @@ impl Drk {
|
||||
}
|
||||
|
||||
/// Broadcast a given transaction to darkfid and forward onto the network.
|
||||
/// Returns the transaction ID upon success
|
||||
/// Returns the transaction ID upon success.
|
||||
pub async fn broadcast_tx(&self, tx: &Transaction) -> Result<String> {
|
||||
println!("Broadcasting transaction...");
|
||||
|
||||
@@ -314,7 +314,7 @@ impl Drk {
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
/// Queries darkfid for a tx with given hash
|
||||
/// Queries darkfid for a tx with given hash.
|
||||
pub async fn get_tx(&self, tx_hash: &TransactionHash) -> Result<Option<Transaction>> {
|
||||
let tx_hash_str = tx_hash.to_string();
|
||||
let req = JsonRequest::new(
|
||||
@@ -333,7 +333,7 @@ impl Drk {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simulate the transaction with the state machine
|
||||
/// Simulate the transaction with the state machine.
|
||||
pub async fn simulate_tx(&self, tx: &Transaction) -> Result<bool> {
|
||||
let tx_str = base64::encode(&serialize_async(tx).await);
|
||||
let req =
|
||||
@@ -361,4 +361,18 @@ impl Drk {
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Queries darkfid for given transaction's gas.
|
||||
pub async fn get_tx_gas(&self, tx: &Transaction, include_fee: bool) -> Result<u64> {
|
||||
let params = JsonValue::Array(vec![
|
||||
JsonValue::String(base64::encode(&serialize_async(tx).await)),
|
||||
JsonValue::Boolean(include_fee),
|
||||
]);
|
||||
let req = JsonRequest::new("tx.calculate_gas", params);
|
||||
let rep = self.rpc_client.as_ref().unwrap().request(req).await?;
|
||||
|
||||
let gas = *rep.get::<f64>().unwrap() as u64;
|
||||
|
||||
Ok(gas)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,11 @@ use darkfi::{
|
||||
Error, Result,
|
||||
};
|
||||
use darkfi_money_contract::{
|
||||
client::{transfer_v1::make_transfer_call, OwnCoin},
|
||||
model::TokenId,
|
||||
MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
|
||||
client::transfer_v1::make_transfer_call, model::TokenId, MoneyFunction,
|
||||
MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_FEE_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
|
||||
};
|
||||
use darkfi_sdk::{
|
||||
crypto::{contract_id::MONEY_CONTRACT_ID, FuncId, Keypair, PublicKey},
|
||||
crypto::{contract_id::MONEY_CONTRACT_ID, Keypair, PublicKey},
|
||||
tx::ContractCall,
|
||||
};
|
||||
use darkfi_serial::AsyncEncodable;
|
||||
@@ -44,13 +43,8 @@ impl Drk {
|
||||
token_id: TokenId,
|
||||
recipient: PublicKey,
|
||||
) -> Result<Transaction> {
|
||||
// First get all unspent OwnCoins to see what our balance is.
|
||||
let owncoins = self.get_coins(false).await?;
|
||||
let mut owncoins: Vec<OwnCoin> = owncoins.iter().map(|x| x.0.clone()).collect();
|
||||
// We're only interested in the ones for the token_id we're sending
|
||||
// And the ones not owned by some protocol (meaning spend-hook should be 0)
|
||||
owncoins.retain(|x| x.note.token_id == token_id);
|
||||
owncoins.retain(|x| x.note.spend_hook == FuncId::none());
|
||||
// First get all unspent OwnCoins to see what our balance is
|
||||
let owncoins = self.get_token_coins(&token_id).await?;
|
||||
if owncoins.is_empty() {
|
||||
return Err(Error::Custom(format!("Did not find any coins with token ID: {token_id}")))
|
||||
}
|
||||
@@ -74,12 +68,10 @@ impl Drk {
|
||||
let secret = self.default_secret().await?;
|
||||
let keypair = Keypair::new(secret);
|
||||
|
||||
let contract_id = *MONEY_CONTRACT_ID;
|
||||
|
||||
// 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(&contract_id).await?;
|
||||
let zkas_bins = self.lookup_zkas(&MONEY_CONTRACT_ID).await?;
|
||||
|
||||
let Some(mint_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1)
|
||||
else {
|
||||
@@ -91,38 +83,71 @@ impl Drk {
|
||||
return Err(Error::Custom("Burn circuit not found".to_string()))
|
||||
};
|
||||
|
||||
let Some(fee_zkbin) = zkas_bins.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_FEE_NS_V1)
|
||||
else {
|
||||
return Err(Error::Custom("Fee circuit not found".to_string()))
|
||||
};
|
||||
|
||||
let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
|
||||
let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
|
||||
let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
|
||||
|
||||
let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
|
||||
let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
|
||||
let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
|
||||
|
||||
// Creating Mint and Burn circuit proving keys
|
||||
// Creating Mint, Burn and Fee circuits proving keys
|
||||
let mint_pk = ProvingKey::build(mint_zkbin.k, &mint_circuit);
|
||||
let burn_pk = ProvingKey::build(burn_zkbin.k, &burn_circuit);
|
||||
let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
|
||||
|
||||
// Building transaction parameters
|
||||
let (params, secrets, spent_coins) = make_transfer_call(
|
||||
keypair, recipient, amount, token_id, owncoins, tree, mint_zkbin, mint_pk, burn_zkbin,
|
||||
keypair,
|
||||
recipient,
|
||||
amount,
|
||||
token_id,
|
||||
owncoins,
|
||||
tree.clone(),
|
||||
mint_zkbin,
|
||||
mint_pk,
|
||||
burn_zkbin,
|
||||
burn_pk,
|
||||
)?;
|
||||
|
||||
// Encode and sign the transaction
|
||||
// Encode the call
|
||||
let mut data = vec![MoneyFunction::TransferV1 as u8];
|
||||
params.encode_async(&mut data).await?;
|
||||
let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
|
||||
|
||||
// Create the TransactionBuilder containing the `Transfer` call
|
||||
let mut tx_builder =
|
||||
TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, 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.
|
||||
// We also tell it about any spent coins so we don't accidentally reuse them in the
|
||||
// fee call.
|
||||
// TODO: We have to build a proper coin selection algorithm so that we can utilize
|
||||
// the Money::Transfer to merge any coins which would give us a coin with enough
|
||||
// value for paying the transaction fee.
|
||||
let mut tx = tx_builder.build()?;
|
||||
let sigs = tx.create_sigs(&secrets.signature_secrets)?;
|
||||
tx.signatures = vec![sigs];
|
||||
tx.signatures.push(sigs);
|
||||
|
||||
// We need to mark the coins we've spent in our wallet
|
||||
for spent_coin in spent_coins {
|
||||
if let Err(e) = self.mark_spent_coin(&spent_coin.coin).await {
|
||||
return Err(Error::Custom(format!("Mark spent coin {spent_coin:?} failed: {e:?}")))
|
||||
};
|
||||
}
|
||||
let (fee_call, fee_proofs, fee_secrets) = self
|
||||
.append_fee_call(&tx, keypair.public, &tree, &fee_pk, &fee_zkbin, Some(&spent_coins))
|
||||
.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(&secrets.signature_secrets)?;
|
||||
tx.signatures.push(sigs);
|
||||
let sigs = tx.create_sigs(&fee_secrets)?;
|
||||
tx.signatures.push(sigs);
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
@@ -40,39 +40,39 @@ testnet user guide.
|
||||
Note: List is not exhaustive. Missing functionalities that are not part
|
||||
of the guide can be added for future regressions.
|
||||
|
||||
| # | Description | Command | Status |
|
||||
|----|---------------------------|--------------------------------------------------|-------------------------|
|
||||
| 0 | Initialization | wallet --initialize | Pass |
|
||||
| 1 | Key generation | wallet --keygen | Pass |
|
||||
| 2 | Set default wallet | wallet --default-address {ADDR_ID} | Pass |
|
||||
| 3 | Default address retrieval | wallet --address | Pass |
|
||||
| 4 | Block scanning | scan | Pass |
|
||||
| 5 | Block subscribing | subscribe | Pass |
|
||||
| 6 | Balance retrieval | wallet --balance | Pass |
|
||||
| 7 | Aliases retrieval | alias show | Pass |
|
||||
| 8 | Mint auth generation | token generate-mint | Pass |
|
||||
| 9 | Mint auths retrieval | token list | Pass |
|
||||
| 10 | Alias add | alias add {ALIAS} {TOKEN} | Pass |
|
||||
| 11 | Aliases retrieval | alias show | Pass |
|
||||
| 12 | Mint generation | token mint {ALIAS} {AMOUNT} {ADDR} | Failure: disabled |
|
||||
| 13 | Transfer | transfer {AMOUNT} {ALIAS} {ADDR} | Failure: fee is missing |
|
||||
| 14 | Coins retrieval | wallet --coins | Pass |
|
||||
| 15 | OTC initialization | otc init -v {AMOUNT}:{AMOUNT} -t {ALIAS}:{ALIAS} | Failure: needs #12 |
|
||||
| 16 | OTC join | otc join | Failure: needs #15 |
|
||||
| 17 | OTC sign | otc sign | Failure: needs #16 |
|
||||
| 18 | DAO create | dao create {LIMIT} {QUORUM} {RATIO} {TOKEN} | Failure: needs #12 |
|
||||
| 19 | DAO view | dao view | Failure: needs #18 |
|
||||
| 20 | DAO import | dao import | Failure: needs #18 |
|
||||
| 21 | DAO list | dao sign | Failure: needs #18 |
|
||||
| 22 | DAO mint | dao mint {DAO} | Failure: needs #18 |
|
||||
| 23 | DAO balance | dao balance {DAO} | Failure: needs #18 |
|
||||
| 24 | DAO propose | dao propose {DAO} {ADDR} {AMOUNT} {TOKEN} | Failure: needs #18 |
|
||||
| 25 | DAO proposals retrieval | dao proposals {DAO} | Failure: needs #24 |
|
||||
| 26 | DAO proposal retrieval | dao proposal {DAO} {PROPOSAL_ID} | Failure: needs #24 |
|
||||
| 27 | DAO vote | dao vote {DAO} {PROPOSAL_ID} {VOTE} {WEIGHT} | Failure: needs #24 |
|
||||
| 28 | DAO proposal execution | dao exec {DAO} {PROPOSAL_ID} | Failure: needs #27 |
|
||||
| 29 | Coins unspend | unspend {COIN} | Pass |
|
||||
| 30 | Transaction inspect | inspect | Pass |
|
||||
| 31 | Transaction simulate | explorer simulate-tx | Pass |
|
||||
| 31 | Transaction broadcast | broadcast | Pass |
|
||||
| # | Description | Command | Status |
|
||||
|----|---------------------------|--------------------------------------------------|--------------------|
|
||||
| 0 | Initialization | wallet --initialize | Pass |
|
||||
| 1 | Key generation | wallet --keygen | Pass |
|
||||
| 2 | Set default wallet | wallet --default-address {ADDR_ID} | Pass |
|
||||
| 3 | Default address retrieval | wallet --address | Pass |
|
||||
| 4 | Block scanning | scan | Pass |
|
||||
| 5 | Block subscribing | subscribe | Pass |
|
||||
| 6 | Balance retrieval | wallet --balance | Pass |
|
||||
| 7 | Aliases retrieval | alias show | Pass |
|
||||
| 8 | Mint auth generation | token generate-mint | Pass |
|
||||
| 9 | Mint auths retrieval | token list | Pass |
|
||||
| 10 | Alias add | alias add {ALIAS} {TOKEN} | Pass |
|
||||
| 11 | Aliases retrieval | alias show | Pass |
|
||||
| 12 | Mint generation | token mint {ALIAS} {AMOUNT} {ADDR} | Failure: disabled |
|
||||
| 13 | Transfer | transfer {AMOUNT} {ALIAS} {ADDR} | Pass |
|
||||
| 14 | Coins retrieval | wallet --coins | Pass |
|
||||
| 15 | OTC initialization | otc init -v {AMOUNT}:{AMOUNT} -t {ALIAS}:{ALIAS} | Failure: needs #12 |
|
||||
| 16 | OTC join | otc join | Failure: needs #15 |
|
||||
| 17 | OTC sign | otc sign | Failure: needs #16 |
|
||||
| 18 | DAO create | dao create {LIMIT} {QUORUM} {RATIO} {TOKEN} | Failure: needs #12 |
|
||||
| 19 | DAO view | dao view | Failure: needs #18 |
|
||||
| 20 | DAO import | dao import | Failure: needs #18 |
|
||||
| 21 | DAO list | dao sign | Failure: needs #18 |
|
||||
| 22 | DAO mint | dao mint {DAO} | Failure: needs #18 |
|
||||
| 23 | DAO balance | dao balance {DAO} | Failure: needs #18 |
|
||||
| 24 | DAO propose | dao propose {DAO} {ADDR} {AMOUNT} {TOKEN} | Failure: needs #18 |
|
||||
| 25 | DAO proposals retrieval | dao proposals {DAO} | Failure: needs #24 |
|
||||
| 26 | DAO proposal retrieval | dao proposal {DAO} {PROPOSAL_ID} | Failure: needs #24 |
|
||||
| 27 | DAO vote | dao vote {DAO} {PROPOSAL_ID} {VOTE} {WEIGHT} | Failure: needs #24 |
|
||||
| 28 | DAO proposal execution | dao exec {DAO} {PROPOSAL_ID} | Failure: needs #27 |
|
||||
| 29 | Coins unspend | unspend {COIN} | Pass |
|
||||
| 30 | Transaction inspect | inspect | Pass |
|
||||
| 31 | Transaction simulate | explorer simulate-tx | Pass |
|
||||
| 31 | Transaction broadcast | broadcast | Pass |
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use darkfi_sdk::crypto::MerkleTree;
|
||||
use log::{debug, error, info, warn};
|
||||
@@ -30,6 +30,7 @@ use crate::{
|
||||
},
|
||||
error::TxVerifyFailed,
|
||||
tx::Transaction,
|
||||
zk::VerifyingKey,
|
||||
Error, Result,
|
||||
};
|
||||
|
||||
@@ -45,7 +46,7 @@ use pow::PoWModule;
|
||||
pub mod verification;
|
||||
use verification::{
|
||||
verify_block, verify_checkpoint_block, verify_genesis_block, verify_producer_transaction,
|
||||
verify_transactions,
|
||||
verify_transaction, verify_transactions,
|
||||
};
|
||||
|
||||
/// Fee calculation helpers
|
||||
@@ -53,7 +54,7 @@ pub mod fees;
|
||||
|
||||
/// Helper utilities
|
||||
pub mod utils;
|
||||
use utils::{block_rank, deploy_native_contracts};
|
||||
use utils::{best_fork_index, block_rank, deploy_native_contracts};
|
||||
|
||||
/// Configuration for initializing [`Validator`]
|
||||
#[derive(Clone)]
|
||||
@@ -127,6 +128,41 @@ impl Validator {
|
||||
Ok(state)
|
||||
}
|
||||
|
||||
/// Auxiliary function to compute provided transaction's total gas,
|
||||
/// against current best fork.
|
||||
/// The function takes a boolean called `verify_fee` to overwrite
|
||||
/// the nodes configured `verify_fees` flag.
|
||||
pub async fn calculate_gas(&self, tx: &Transaction, verify_fee: bool) -> Result<u64> {
|
||||
// Grab the best fork to verify against
|
||||
let forks = self.consensus.forks.read().await;
|
||||
let fork = &forks[best_fork_index(&forks)?];
|
||||
let overlay = fork.overlay.lock().unwrap().full_clone()?;
|
||||
let next_block_height = fork.get_next_block_height()?;
|
||||
drop(forks);
|
||||
|
||||
// Map of ZK proof verifying keys for the transaction
|
||||
let mut vks: HashMap<[u8; 32], HashMap<String, VerifyingKey>> = HashMap::new();
|
||||
for call in &tx.calls {
|
||||
vks.insert(call.data.contract_id.to_bytes(), HashMap::new());
|
||||
}
|
||||
|
||||
// Verify transaction to grab the gas used
|
||||
let verify_result = verify_transaction(
|
||||
&overlay,
|
||||
next_block_height,
|
||||
tx,
|
||||
&mut MerkleTree::new(1),
|
||||
&mut vks,
|
||||
verify_fee,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Purge new trees
|
||||
overlay.lock().unwrap().overlay.lock().unwrap().purge_new_trees()?;
|
||||
|
||||
verify_result
|
||||
}
|
||||
|
||||
/// The node retrieves a transaction, validates its state transition,
|
||||
/// and appends it to the pending txs store.
|
||||
pub async fn append_tx(&self, tx: &Transaction, write: bool) -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user