Преглед изворни кода

contract/money: Implement Money::BurnV1 function

This allows provable coin burning without requiring outputs.
x пре 5 месеци
родитељ
комит
7c5390c899

+ 9 - 2
bin/drk/src/money.rs

@@ -40,8 +40,8 @@ use darkfi_money_contract::{
         MoneyNote, OwnCoin,
     },
     model::{
-        Coin, Input, MoneyAuthTokenFreezeParamsV1, MoneyAuthTokenMintParamsV1, MoneyFeeParamsV1,
-        MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenMintParamsV1,
+        Coin, Input, MoneyAuthTokenFreezeParamsV1, MoneyAuthTokenMintParamsV1, MoneyBurnParamsV1,
+        MoneyFeeParamsV1, MoneyGenesisMintParamsV1, MoneyPoWRewardParamsV1, MoneyTokenMintParamsV1,
         MoneyTransferParamsV1, Nullifier, Output, TokenId, DARK_TOKEN_ID,
     },
     MoneyFunction, MONEY_CONTRACT_ZKAS_FEE_NS_V1,
@@ -806,6 +806,13 @@ impl Drk {
                     deserialize_async(&child_call.data.data[1..]).await?;
                 coins.push((params.coin, child_params.enc_note, false));
             }
+            MoneyFunction::BurnV1 => {
+                scan_cache.log(String::from("[parse_money_call] Found Money::BurnV1 call"));
+                let params: MoneyBurnParamsV1 = deserialize_async(&data[1..]).await?;
+                for input in params.inputs {
+                    nullifiers.push(input.nullifier);
+                }
+            }
         }
 
         Ok((nullifiers, coins, freezes))

+ 2 - 2
src/contract/money/Makefile

@@ -61,11 +61,11 @@ test-genesis-mint: all
 		--features=no-entrypoint,client \
 		--test genesis_mint
 
-test-token-mint: all
+test-token-mint-burn: all
 	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) test --target=$(RUST_TARGET) \
 		--release --package $(PKGNAME) \
 		--features=no-entrypoint,client \
-		--test token_mint
+		--test token_mint_burn
 
 test-delayed-tx: all
 	RUSTFLAGS="$(RUSTFLAGS)" $(CARGO) test --target=$(RUST_TARGET) \

+ 156 - 0
src/contract/money/src/client/burn_v1.rs

@@ -0,0 +1,156 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::{
+    zk::{Proof, ProvingKey},
+    zkas::ZkBinary,
+    ClientFailed, Result,
+};
+use darkfi_sdk::crypto::{BaseBlind, Blind, MerkleTree, ScalarBlind, SecretKey};
+use rand::rngs::OsRng;
+use tracing::debug;
+
+use crate::{
+    client::{
+        transfer_v1::{proof::create_transfer_burn_proof, TransferCallInput},
+        OwnCoin,
+    },
+    error::MoneyError,
+    model::{Input, MoneyBurnParamsV1},
+};
+
+/// Struct holding necessary information to build a `Money::BurnV1`
+/// contract call.
+pub struct BurnCallBuilder {
+    /// Anonymous inputs
+    pub inputs: Vec<TransferCallInput>,
+    /// `Burn_V1` zkas circuit ZkBinary
+    pub burn_zkbin: ZkBinary,
+    /// Proving key for the `Burn_V1` zk circuit
+    pub burn_pk: ProvingKey,
+}
+
+impl BurnCallBuilder {
+    pub fn build(self) -> Result<(MoneyBurnParamsV1, BurnCallDebris)> {
+        debug!(target: "contract::money::client::burn::build", "Building Money::BurnV1 contract call");
+        if self.inputs.is_empty() {
+            return Err(ClientFailed::VerifyError(MoneyError::BurnMissingInputs.to_string()).into())
+        }
+
+        let mut params = MoneyBurnParamsV1 { inputs: vec![] };
+        let mut signature_secrets = vec![];
+        let mut proofs = vec![];
+
+        let token_blind = BaseBlind::random(&mut OsRng);
+        let mut input_value_blinds = vec![];
+
+        debug!(target: "contract::money::client::burn::build", "Building anonymous inputs");
+        for (i, input) in self.inputs.iter().enumerate() {
+            let value_blind = Blind::random(&mut OsRng);
+            input_value_blinds.push(value_blind);
+
+            let signature_secret = SecretKey::random(&mut OsRng);
+            signature_secrets.push(signature_secret);
+
+            debug!(target: "contract::money::client::burn::build", "Creating burn proof for input {i}");
+            let (proof, public_inputs) = create_transfer_burn_proof(
+                &self.burn_zkbin,
+                &self.burn_pk,
+                input,
+                value_blind,
+                token_blind,
+                signature_secret,
+            )?;
+
+            params.inputs.push(Input {
+                value_commit: public_inputs.value_commit,
+                token_commit: public_inputs.token_commit,
+                nullifier: public_inputs.nullifier,
+                merkle_root: public_inputs.merkle_root,
+                user_data_enc: public_inputs.user_data_enc,
+                signature_public: public_inputs.signature_public,
+            });
+
+            proofs.push(proof);
+        }
+
+        let secrets = BurnCallDebris { proofs, signature_secrets, input_value_blinds, token_blind };
+        Ok((params, secrets))
+    }
+}
+
+pub struct BurnCallDebris {
+    /// The ZK proofs created in this builder
+    pub proofs: Vec<Proof>,
+    /// The ephemeral secret keys created for signing
+    pub signature_secrets: Vec<SecretKey>,
+    /// The value blinds created for each input
+    pub input_value_blinds: Vec<ScalarBlind>,
+    /// The token blind used for all inputs
+    pub token_blind: BaseBlind,
+}
+
+/// Make a simple burn call to permanently destroy coins.
+///
+/// * `coins`: Set of `OwnCoin` we're given to burn in this call
+/// * `tree`: Merkle tree of coins used to create inclusion proofs
+/// * `burn_zkbin`: `Burn_V1` zkas circuit ZkBinary
+/// * `burn_pk`: Proving key for the `Burn_V1` zk circuit
+///
+/// Returns a tuple of:
+///
+/// * The actual call data
+/// * Secret values such as blinds
+/// * A list of the spent coins
+pub fn make_burn_call(
+    coins: Vec<OwnCoin>,
+    tree: MerkleTree,
+    burn_zkbin: ZkBinary,
+    burn_pk: ProvingKey,
+) -> Result<(MoneyBurnParamsV1, BurnCallDebris, Vec<OwnCoin>)> {
+    debug!(target: "contract::money::client::burn", "Building Money::BurnV1 contract call");
+
+    if coins.is_empty() {
+        return Err(ClientFailed::VerifyError(MoneyError::BurnMissingInputs.to_string()).into())
+    }
+
+    // Ensure the coins given to us are all of the same token ID.
+    let token_id = coins[0].note.token_id;
+    for coin in &coins {
+        if coin.note.token_id != token_id {
+            return Err(ClientFailed::InvalidTokenId(coin.note.token_id.to_string()).into())
+        }
+    }
+
+    let mut inputs = vec![];
+    for coin in coins.iter() {
+        let input = TransferCallInput {
+            coin: coin.clone(),
+            merkle_path: tree.witness(coin.leaf_position, 0).unwrap(),
+            user_data_blind: Blind::random(&mut OsRng),
+        };
+
+        inputs.push(input);
+    }
+
+    let burn_builder = BurnCallBuilder { inputs, burn_zkbin, burn_pk };
+
+    let (params, secrets) = burn_builder.build()?;
+
+    Ok((params, secrets, coins))
+}

+ 3 - 0
src/contract/money/src/client/mod.rs

@@ -66,6 +66,9 @@ pub mod auth_token_freeze_v1;
 /// `Money::TokenMintV1` API
 pub mod token_mint_v1;
 
+/// `Money::BurnV1` API
+pub mod burn_v1;
+
 /// `MoneyNote` holds the inner attributes of a `Coin`.
 ///
 /// It does not store the public key since it's encrypted for that key,

+ 15 - 2
src/contract/money/src/entrypoint.rs

@@ -29,8 +29,8 @@ use darkfi_serial::{deserialize, serialize, Encodable, WriteExt};
 use crate::{
     error::MoneyError,
     model::{
-        MoneyAuthTokenFreezeUpdateV1, MoneyAuthTokenMintUpdateV1, MoneyFeeUpdateV1,
-        MoneyGenesisMintUpdateV1, MoneyPoWRewardUpdateV1, MoneyTokenMintUpdateV1,
+        MoneyAuthTokenFreezeUpdateV1, MoneyAuthTokenMintUpdateV1, MoneyBurnUpdateV1,
+        MoneyFeeUpdateV1, MoneyGenesisMintUpdateV1, MoneyPoWRewardUpdateV1, MoneyTokenMintUpdateV1,
         MoneyTransferUpdateV1,
     },
     MoneyFunction, EMPTY_COINS_TREE_ROOT, MONEY_CONTRACT_COINS_TREE,
@@ -95,6 +95,12 @@ use token_mint_v1::{
     money_token_mint_process_update_v1,
 };
 
+/// `Money::Burn` functions
+mod burn_v1;
+use burn_v1::{
+    money_burn_get_metadata_v1, money_burn_process_instruction_v1, money_burn_process_update_v1,
+};
+
 darkfi_sdk::define_contract!(
     init: init_contract,
     exec: process_instruction,
@@ -250,6 +256,7 @@ fn get_metadata(cid: ContractId, ix: &[u8]) -> ContractResult {
             money_auth_token_freeze_get_metadata_v1(cid, call_idx, calls)?
         }
         MoneyFunction::TokenMintV1 => money_token_mint_get_metadata_v1(cid, call_idx, calls)?,
+        MoneyFunction::BurnV1 => money_burn_get_metadata_v1(cid, call_idx, calls)?,
     };
 
     wasm::util::set_return_data(&metadata)
@@ -290,6 +297,7 @@ fn process_instruction(cid: ContractId, ix: &[u8]) -> ContractResult {
         MoneyFunction::TokenMintV1 => {
             money_token_mint_process_instruction_v1(cid, call_idx, calls)?
         }
+        MoneyFunction::BurnV1 => money_burn_process_instruction_v1(cid, call_idx, calls)?,
     };
 
     wasm::util::set_return_data(&update_data)
@@ -343,5 +351,10 @@ fn process_update(cid: ContractId, update_data: &[u8]) -> ContractResult {
             let update: MoneyTokenMintUpdateV1 = deserialize(&update_data[1..])?;
             Ok(money_token_mint_process_update_v1(cid, update)?)
         }
+
+        MoneyFunction::BurnV1 => {
+            let update: MoneyBurnUpdateV1 = deserialize(&update_data[1..])?;
+            Ok(money_burn_process_update_v1(cid, update)?)
+        }
     }
 }

+ 193 - 0
src/contract/money/src/entrypoint/burn_v1.rs

@@ -0,0 +1,193 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi_sdk::{
+    crypto::{
+        pasta_prelude::*,
+        smt::{
+            wasmdb::{SmtWasmDbStorage, SmtWasmFp},
+            PoseidonFp, EMPTY_NODES_FP,
+        },
+        ContractId, FuncId, FuncRef, PublicKey,
+    },
+    dark_tree::DarkLeaf,
+    error::{ContractError, ContractResult},
+    msg,
+    pasta::pallas,
+    wasm, ContractCall,
+};
+use darkfi_serial::{deserialize, serialize, Encodable};
+
+use crate::{
+    error::MoneyError,
+    model::{MoneyBurnParamsV1, MoneyBurnUpdateV1},
+    MONEY_CONTRACT_COIN_ROOTS_TREE, MONEY_CONTRACT_INFO_TREE, MONEY_CONTRACT_LATEST_NULLIFIER_ROOT,
+    MONEY_CONTRACT_NULLIFIERS_TREE, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE,
+    MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+};
+
+/// `get_metadata` function for `Money::BurnV1`
+pub(crate) fn money_burn_get_metadata_v1(
+    _cid: ContractId,
+    call_idx: usize,
+    calls: Vec<DarkLeaf<ContractCall>>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx].data;
+    let params: MoneyBurnParamsV1 = deserialize(&self_.data[1..])?;
+
+    // Public inputs for the ZK proofs we have to verify
+    let mut zk_public_inputs: Vec<(String, Vec<pallas::Base>)> = vec![];
+    // Public keys for the transaction signatures we have to verify
+    let mut signature_pubkeys: Vec<PublicKey> = vec![];
+
+    // Calculate the spend hook
+    let spend_hook = match calls[call_idx].parent_index {
+        Some(parent_idx) => {
+            let parent_call = &calls[parent_idx].data;
+            let contract_id = parent_call.contract_id;
+            let func_code = parent_call.data[0];
+
+            FuncRef { contract_id, func_code }.to_func_id()
+        }
+        None => FuncId::none(),
+    };
+
+    // Grab the pedersen commitments and signature pubkeys from the
+    // anonymous inputs
+    for input in &params.inputs {
+        let value_coords = input.value_commit.to_affine().coordinates().unwrap();
+        let (sig_x, sig_y) = input.signature_public.xy();
+
+        // It is very important that these are in the same order as the
+        // `constrain_instance` calls in the zkas code.
+        // Otherwise verification will fail.
+        zk_public_inputs.push((
+            MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(),
+            vec![
+                input.nullifier.inner(),
+                *value_coords.x(),
+                *value_coords.y(),
+                input.token_commit,
+                input.merkle_root.inner(),
+                input.user_data_enc,
+                spend_hook.inner(),
+                sig_x,
+                sig_y,
+            ],
+        ));
+
+        signature_pubkeys.push(input.signature_public);
+    }
+
+    // No outputs - this is a burn, value is destroyed.
+
+    // Serialize everything gathered and return it
+    let mut metadata = vec![];
+    zk_public_inputs.encode(&mut metadata)?;
+    signature_pubkeys.encode(&mut metadata)?;
+
+    Ok(metadata)
+}
+
+/// `process_instruction` function for `Money::BurnV1`
+pub(crate) fn money_burn_process_instruction_v1(
+    cid: ContractId,
+    call_idx: usize,
+    calls: Vec<DarkLeaf<ContractCall>>,
+) -> Result<Vec<u8>, ContractError> {
+    let self_ = &calls[call_idx];
+    let params: MoneyBurnParamsV1 = deserialize(&self_.data.data[1..])?;
+
+    if params.inputs.is_empty() {
+        msg!("[BurnV1] Error: No inputs in the call");
+        return Err(MoneyError::BurnMissingInputs.into())
+    }
+
+    // Access the necessary databases where there is information to
+    // validate this state transition.
+    let nullifiers_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
+    let coin_roots_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_COIN_ROOTS_TREE)?;
+
+    let hasher = PoseidonFp::new();
+    let empty_leaf = pallas::Base::ZERO;
+    let smt_store = SmtWasmDbStorage::new(nullifiers_db);
+    let smt = SmtWasmFp::new(smt_store, hasher, &EMPTY_NODES_FP);
+
+    // Grab the expected token commitment. All inputs must use the
+    // same token type.
+    let tokcom = params.inputs[0].token_commit;
+
+    // ===================================
+    // Perform the actual state transition
+    // ===================================
+
+    let mut new_nullifiers = Vec::with_capacity(params.inputs.len());
+
+    msg!("[BurnV1] Iterating over anonymous inputs");
+    for (i, input) in params.inputs.iter().enumerate() {
+        // The Merkle root is used to know whether this is a coin that
+        // existed in a previous state.
+        if !wasm::db::db_contains_key(coin_roots_db, &serialize(&input.merkle_root))? {
+            msg!("[BurnV1] Error: Merkle root not found in previous state (input {})", i);
+            return Err(MoneyError::TransferMerkleRootNotFound.into())
+        }
+
+        // The nullifiers should not already exist. It is the double-spend protection.
+        if new_nullifiers.contains(&input.nullifier) ||
+            smt.get_leaf(&input.nullifier.inner()) != empty_leaf
+        {
+            msg!("[BurnV1] Error: Duplicate nullifier found in input {}", i);
+            return Err(MoneyError::DuplicateNullifier.into())
+        }
+
+        // Verify the token commitment is the expected one
+        if tokcom != input.token_commit {
+            msg!("[BurnV1] Error: Token commitment mismatch in input {}", i);
+            return Err(MoneyError::TokenMismatch.into())
+        }
+
+        new_nullifiers.push(input.nullifier);
+    }
+
+    // No outputs, no value commitment balance check.
+    // The value committed in the inputs is permanently destroyed.
+
+    let update = MoneyBurnUpdateV1 { nullifiers: new_nullifiers };
+    Ok(serialize(&update))
+}
+
+/// `process_update` function for `Money::BurnV1`
+pub(crate) fn money_burn_process_update_v1(
+    cid: ContractId,
+    update: MoneyBurnUpdateV1,
+) -> ContractResult {
+    let info_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_INFO_TREE)?;
+    let nullifiers_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_NULLIFIERS_TREE)?;
+    let nullifier_roots_db = wasm::db::db_lookup(cid, MONEY_CONTRACT_NULLIFIER_ROOTS_TREE)?;
+
+    msg!("[BurnV1] Adding new nullifiers to the set");
+    wasm::merkle::sparse_merkle_insert_batch(
+        info_db,
+        nullifiers_db,
+        nullifier_roots_db,
+        MONEY_CONTRACT_LATEST_NULLIFIER_ROOT,
+        &update.nullifiers.iter().map(|n| n.inner()).collect::<Vec<_>>(),
+    )?;
+
+    Ok(())
+}

+ 4 - 0
src/contract/money/src/error.rs

@@ -109,6 +109,9 @@ pub enum MoneyError {
 
     #[error("Children indexes length missmatch")]
     ChildrenIndexesLengthMismatch,
+
+    #[error("Missing inputs in burn call")]
+    BurnMissingInputs,
 }
 
 impl From<MoneyError> for ContractError {
@@ -143,6 +146,7 @@ impl From<MoneyError> for ContractError {
             MoneyError::CoinMerkleRootNotFound => Self::Custom(27),
             MoneyError::RootsValueDataMismatch => Self::Custom(28),
             MoneyError::ChildrenIndexesLengthMismatch => Self::Custom(29),
+            MoneyError::BurnMissingInputs => Self::Custom(30),
         }
     }
 }

+ 2 - 0
src/contract/money/src/lib.rs

@@ -36,6 +36,7 @@ pub enum MoneyFunction {
     AuthTokenMintV1 = 0x05,
     AuthTokenFreezeV1 = 0x06,
     TokenMintV1 = 0x07,
+    BurnV1 = 0x08,
 }
 // ANCHOR_END: money-function
 
@@ -52,6 +53,7 @@ impl TryFrom<u8> for MoneyFunction {
             0x05 => Ok(Self::AuthTokenMintV1),
             0x06 => Ok(Self::AuthTokenFreezeV1),
             0x07 => Ok(Self::TokenMintV1),
+            0x08 => Ok(Self::BurnV1),
             _ => Err(ContractError::InvalidFunction),
         }
     }

+ 18 - 0
src/contract/money/src/model/mod.rs

@@ -270,6 +270,24 @@ pub struct MoneyAuthTokenFreezeUpdateV1 {
     pub token_id: TokenId,
 }
 
+/// Parameters for `Money::BurnV1`
+///
+/// Burns (destroys) coins, removing value from circulation permanently.
+/// The call has inputs but no outputs; the value committed in the inputs
+/// is destroyed. All inputs must use the same token commitment.
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyBurnParamsV1 {
+    /// Anonymous inputs
+    pub inputs: Vec<Input>,
+}
+
+/// State update for `Money::BurnV1`
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct MoneyBurnUpdateV1 {
+    /// Revealed nullifiers from the burned coins
+    pub nullifiers: Vec<Nullifier>,
+}
+
 /// Parameters for `Money::PoWReward`
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct MoneyPoWRewardParamsV1 {

+ 7 - 1
src/contract/money/tests/token_mint.rs → src/contract/money/tests/token_mint_burn.rs

@@ -21,7 +21,7 @@ use darkfi_contract_test_harness::{init_logger, Holder, TestHarness};
 use tracing::info;
 
 #[test]
-fn token_mint() -> Result<()> {
+fn token_mint_burn() -> Result<()> {
     smol::block_on(async {
         init_logger();
 
@@ -44,6 +44,12 @@ fn token_mint() -> Result<()> {
         info!("Freezing BOB token authority");
         th.token_freeze_to_all(&Bob, block_height).await?;
 
+        // Burn the BOB tokens (single coin supply)
+        info!("Burning BOB token");
+        let bob_coins = th.coins(&Bob).to_vec();
+        th.burn_to_all(&Bob, &bob_coins, block_height).await?;
+        assert!(th.coins(&Bob).is_empty());
+
         // Thanks for reading
         Ok(())
     })

+ 21 - 0
src/contract/test-harness/src/lib.rs

@@ -66,6 +66,8 @@ use tracing::{debug, warn};
 /// Utility module for caching ZK proof PKs and VKs
 pub mod vks;
 
+/// `Money::Burn` functionality
+mod money_burn;
 /// `Money::Fee` functionality
 mod money_fee;
 /// `Money::GenesisMint` functionality
@@ -537,6 +539,25 @@ impl TestHarness {
         Ok(())
     }
 
+    /// Burn given [`OwnCoin`]s and execute the tx on all registered holders.
+    pub async fn burn_to_all(
+        &mut self,
+        holder: &Holder,
+        coins: &[OwnCoin],
+        block_height: u32,
+    ) -> Result<()> {
+        let (tx, (params, fee_params), _spent) = self.burn(holder, coins, block_height).await?;
+
+        let holders = self.holder_keys.clone();
+        for h in &holders {
+            self.execute_burn_tx(h, tx.clone(), &params, &fee_params, block_height, true).await?;
+        }
+
+        self.assert_all_trees();
+
+        Ok(())
+    }
+
     /// Build a genesis mint for `holder` and execute on all registered holders.
     /// Returns the found [`OwnCoin`]s.
     pub async fn genesis_mint_to_all(

+ 112 - 0
src/contract/test-harness/src/money_burn.rs

@@ -0,0 +1,112 @@
+/* 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 <https://www.gnu.org/licenses/>.
+ */
+
+use darkfi::{
+    tx::{ContractCallLeaf, Transaction, TransactionBuilder},
+    Result,
+};
+use darkfi_money_contract::{
+    client::{burn_v1::make_burn_call, OwnCoin},
+    model::{MoneyBurnParamsV1, MoneyFeeParamsV1},
+    MoneyFunction, MONEY_CONTRACT_ZKAS_BURN_NS_V1,
+};
+use darkfi_sdk::{crypto::contract_id::MONEY_CONTRACT_ID, ContractCall};
+use darkfi_serial::Encodable;
+
+use super::{Holder, TestHarness};
+
+impl TestHarness {
+    /// Create a `Money::Burn` transaction.
+    pub async fn burn(
+        &mut self,
+        holder: &Holder,
+        owncoins: &[OwnCoin],
+        block_height: u32,
+    ) -> Result<(Transaction, (MoneyBurnParamsV1, Option<MoneyFeeParamsV1>), Vec<OwnCoin>)> {
+        let wallet = self.wallet(holder);
+
+        let (burn_pk, burn_zkbin) = self.proving_keys.get(MONEY_CONTRACT_ZKAS_BURN_NS_V1).unwrap();
+
+        // Create the burn call
+        let (params, secrets, mut spent_coins) = make_burn_call(
+            owncoins.to_owned(),
+            wallet.money_merkle_tree.clone(),
+            burn_zkbin.clone(),
+            burn_pk.clone(),
+        )?;
+
+        let mut data = vec![MoneyFunction::BurnV1 as u8];
+        params.encode(&mut data)?;
+        let call = ContractCall { contract_id: *MONEY_CONTRACT_ID, data };
+
+        let mut tx_builder =
+            TransactionBuilder::new(ContractCallLeaf { call, proofs: secrets.proofs }, vec![])?;
+
+        // Optional fees, if enabled
+        let mut fee_params = None;
+        let mut fee_signature_secrets = None;
+        if self.verify_fees {
+            let mut tx = tx_builder.build()?;
+            let sigs = tx.create_sigs(&secrets.signature_secrets)?;
+            tx.signatures = vec![sigs];
+
+            let (fee_call, fee_proofs, fee_secrets, spent_fee_coins, fee_call_params) =
+                self.append_fee_call(holder, tx, block_height, &spent_coins).await?;
+
+            tx_builder.append(ContractCallLeaf { call: fee_call, proofs: fee_proofs }, vec![])?;
+            fee_signature_secrets = Some(fee_secrets);
+            spent_coins.extend_from_slice(&spent_fee_coins);
+            fee_params = Some(fee_call_params);
+        }
+
+        // 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 = vec![sigs];
+        if let Some(fee_signature_secrets) = fee_signature_secrets {
+            let sigs = tx.create_sigs(&fee_signature_secrets)?;
+            tx.signatures.push(sigs);
+        }
+
+        Ok((tx, (params, fee_params), spent_coins))
+    }
+
+    /// Execute a `Money::Burn` transaction for a given [`Holder`].
+    pub async fn execute_burn_tx(
+        &mut self,
+        holder: &Holder,
+        tx: Transaction,
+        call_params: &MoneyBurnParamsV1,
+        fee_params: &Option<MoneyFeeParamsV1>,
+        block_height: u32,
+        append: bool,
+    ) -> Result<Vec<OwnCoin>> {
+        let wallet = self.wallet_mut(holder);
+
+        wallet.add_transaction("money::burn", tx, block_height).await?;
+
+        wallet.process_inputs(&call_params.inputs, holder);
+
+        let mut found_owncoins = vec![];
+        if append {
+            found_owncoins.extend(wallet.process_fee(fee_params, holder));
+        }
+
+        Ok(found_owncoins)
+    }
+}

+ 54 - 0
src/sdk/python/src/contract/money/burn_v1.rs

@@ -0,0 +1,54 @@
+/* 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 impl FunctionParams foried 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/>.
+ */
+
+use std::fmt::Write;
+
+use darkfi_money_contract::model as money_model;
+use pyo3::{prelude::PyDictMethods, pyclass, types::PyDict, Py, PyResult, Python};
+
+use super::{impl_py_methods, FunctionParams};
+
+/// [`money_model::MoneyBurnParamsV1`] python binding.
+#[pyclass]
+pub struct MoneyBurnParamsV1(money_model::MoneyBurnParamsV1);
+impl_py_methods!(MoneyBurnParamsV1);
+
+impl FunctionParams for money_model::MoneyBurnParamsV1 {
+    fn to_pydict(&self, py: Python) -> PyResult<Py<PyDict>> {
+        let dict = PyDict::new(py);
+        dict.set_item(
+            "inputs",
+            self.inputs
+                .iter()
+                .map(|input| input.to_pydict(py))
+                .collect::<PyResult<Vec<Py<PyDict>>>>()?,
+        )?;
+        Ok(dict.unbind())
+    }
+
+    fn fmt_pretty(&self, out: &mut String, depth: usize) -> PyResult<()> {
+        let prefix = format!("{}├─ ", "   ".repeat(depth));
+        writeln!(out, "{prefix}inputs:").unwrap();
+        for input in &self.inputs {
+            input.fmt_pretty(out, depth + 2)?;
+            writeln!(out).unwrap();
+        }
+
+        Ok(())
+    }
+}

+ 9 - 0
src/sdk/python/src/contract/money/mod.rs

@@ -60,6 +60,10 @@ pub use token_mint_v1::MoneyTokenMintParamsV1;
 pub mod transfer_v1;
 pub use transfer_v1::MoneyTransferParamsV1;
 
+/// [`MoneyFunction::BurnV1`] function call parameter's bindings.
+pub mod burn_v1;
+pub use burn_v1::MoneyBurnParamsV1;
+
 /// Decodes the parameters of a Money contract function call.
 pub fn decode_money_function_params(
     function_index: u8,
@@ -94,6 +98,10 @@ pub fn decode_money_function_params(
             let params: money_model::MoneyTokenMintParamsV1 = deserialize(&data[1..])?;
             Box::new(params)
         }
+        MoneyFunction::BurnV1 => {
+            let params: money_model::MoneyBurnParamsV1 = deserialize(&data[1..])?;
+            Box::new(params)
+        }
     };
 
     Ok(res)
@@ -192,6 +200,7 @@ pub fn create_module(py: Python) -> PyResult<Bound<PyModule>> {
     submod.add_class::<MoneyPoWRewardParamsV1>()?;
     submod.add_class::<MoneyTokenMintParamsV1>()?;
     submod.add_class::<MoneyTransferParamsV1>()?;
+    submod.add_class::<MoneyBurnParamsV1>()?;
     submod.add_class::<Input>()?;
     submod.add_class::<Output>()?;
     submod.add_class::<ClearInput>()?;