darkfi/tx/
mod.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2026 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::collections::HashMap;
20
21use darkfi_sdk::{
22    crypto::{
23        schnorr::{SchnorrPublic, SchnorrSecret, Signature},
24        PublicKey, SecretKey,
25    },
26    dark_tree::{dark_forest_leaf_vec_integrity_check, DarkForest, DarkLeaf, DarkTree},
27    error::DarkTreeResult,
28    pasta::pallas,
29    tx::{ContractCall, TransactionHash},
30};
31
32#[cfg(feature = "async-serial")]
33use darkfi_serial::async_trait;
34
35use darkfi_serial::{Encodable, SerialDecodable, SerialEncodable};
36use tracing::{debug, error};
37
38use crate::{
39    error::TxVerifyFailed,
40    zk::{proof::VerifyingKey, Proof},
41    Error, Result,
42};
43
44macro_rules! zip {
45    ($x:expr) => ($x);
46    ($x:expr, $($y:expr), +) => (
47        $x.iter().zip(zip!($($y), +))
48    )
49}
50
51// ANCHOR: transaction
52/// A Transaction contains an arbitrary number of `ContractCall` objects,
53/// along with corresponding ZK proofs and Schnorr signatures.
54///
55/// `DarkLeaf` is used to map relations between contract calls in the transaction.
56#[derive(Clone, Default, Eq, PartialEq, SerialEncodable, SerialDecodable)]
57// ANCHOR: transaction-struct
58pub struct Transaction {
59    /// Calls executed in this transaction
60    pub calls: Vec<DarkLeaf<ContractCall>>,
61    /// Attached ZK proofs
62    pub proofs: Vec<Vec<Proof>>,
63    /// Attached Schnorr signatures
64    pub signatures: Vec<Vec<Signature>>,
65}
66// ANCHOR_END: transaction-struct
67// ANCHOR_END: transaction
68
69impl Transaction {
70    /// Verify ZK proofs for the entire transaction.
71    pub async fn verify_zkps(
72        &self,
73        verifying_keys: &HashMap<[u8; 32], HashMap<String, VerifyingKey>>,
74        zkp_table: Vec<Vec<(String, Vec<pallas::Base>)>>,
75    ) -> Result<()> {
76        // TODO: Are we sure we should assert here?
77        assert_eq!(self.calls.len(), self.proofs.len());
78        assert_eq!(self.calls.len(), zkp_table.len());
79
80        for (call, (proofs, pubvals)) in zip!(self.calls, self.proofs, zkp_table) {
81            assert_eq!(proofs.len(), pubvals.len());
82
83            let Some(contract_map) = verifying_keys.get(&call.data.contract_id.to_bytes()) else {
84                error!(
85                    target: "tx::verify_zkps",
86                    "[TX] Verifying keys not found for contract {}",
87                    call.data.contract_id,
88                );
89                return Err(TxVerifyFailed::InvalidZkProof.into())
90            };
91
92            for (proof, (zk_ns, public_vals)) in proofs.iter().zip(pubvals.iter()) {
93                if let Some(vk) = contract_map.get(zk_ns) {
94                    // We have a verifying key for this
95                    debug!(target: "tx::verify_zkps", "[TX] public inputs: {public_vals:#?}");
96                    if let Err(e) = proof.verify(vk, public_vals) {
97                        error!(
98                            target: "tx::verify_zkps",
99                            "[TX] Failed verifying {}::{zk_ns} ZK proof: {e:#?}",
100                            call.data.contract_id
101                        );
102                        return Err(TxVerifyFailed::InvalidZkProof.into())
103                    }
104                    debug!(
105                        target: "tx::verify_zkps",
106                        "[TX] Successfully verified {}::{zk_ns} ZK proof",
107                        call.data.contract_id
108                    );
109                    continue
110                }
111
112                error!(
113                    target: "tx::verify_zkps",
114                    "[TX] {}::{zk_ns} circuit VK nonexistent",
115                    call.data.contract_id
116                );
117                return Err(TxVerifyFailed::InvalidZkProof.into())
118            }
119        }
120
121        Ok(())
122    }
123
124    /// Verify Schnorr signatures for the entire transaction.
125    pub fn verify_sigs(&self, pub_table: Vec<Vec<PublicKey>>) -> Result<()> {
126        // Hash the transaction without the signatures
127        let mut hasher = blake3::Hasher::new();
128        self.calls.encode(&mut hasher)?;
129        self.proofs.encode(&mut hasher)?;
130        let data_hash = hasher.finalize();
131
132        debug!(target: "tx::verify_sigs", "tx.verify_sigs: data_hash: {data_hash}");
133
134        assert_eq!(self.signatures.len(), pub_table.len());
135
136        for (i, (sigs, pubkeys)) in self.signatures.iter().zip(pub_table.iter()).enumerate() {
137            assert_eq!(sigs.len(), pubkeys.len());
138
139            for (pubkey, signature) in pubkeys.iter().zip(sigs) {
140                debug!(target: "tx::verify_sigs", "[TX] Verifying signature with public key: {pubkey}");
141                if !pubkey.verify(&data_hash.as_bytes()[..], signature) {
142                    error!(target: "tx::verify_sigs", "[TX] tx::verify_sigs[{i}] failed to verify signature");
143                    return Err(Error::InvalidSignature)
144                }
145            }
146
147            debug!(target: "tx::verify_sigs", "[TX] tx::verify_sigs[{i}] passed");
148        }
149
150        Ok(())
151    }
152
153    /// Create Schnorr signatures for the entire transaction.
154    pub fn create_sigs(&self, secret_keys: &[SecretKey]) -> Result<Vec<Signature>> {
155        // Hash the transaction without the signatures
156        let mut hasher = blake3::Hasher::new();
157        self.calls.encode(&mut hasher)?;
158        self.proofs.encode(&mut hasher)?;
159        let data_hash = hasher.finalize();
160
161        debug!(target: "tx::create_sigs", "[TX] tx.create_sigs: data_hash: {data_hash}");
162
163        let mut sigs = vec![];
164        for secret in secret_keys {
165            debug!(
166                target: "tx::create_sigs",
167                "[TX] Creating signature with public key: {}", PublicKey::from_secret(*secret),
168            );
169            let signature = secret.sign(&data_hash.as_bytes()[..]);
170            sigs.push(signature);
171        }
172
173        Ok(sigs)
174    }
175
176    /// Get the transaction hash
177    pub fn hash(&self) -> TransactionHash {
178        let mut hasher = blake3::Hasher::new();
179        // Blake3 hasher .update() method never fails.
180        // This call returns a Result due to how the Write trait is specified.
181        // Calling unwrap() here should be safe.
182        self.encode(&mut hasher).expect("blake3 hasher");
183        TransactionHash(hasher.finalize().into())
184    }
185
186    /// Returns true if transaction is a PoW reward one.
187    pub fn is_pow_reward(&self) -> bool {
188        // PoW rewards must be single contract calls
189        if !self.is_single_call() {
190            return false;
191        }
192
193        self.calls[0].data.is_money_pow_reward()
194    }
195
196    /// Returns true if the transaction consists of a single call with non-empty data.
197    pub fn is_single_call(&self) -> bool {
198        self.calls.len() == 1 && !self.calls[0].data.data.is_empty()
199    }
200}
201
202// Avoid showing the proofs and sigs in the debug output since often they are very long.
203impl std::fmt::Debug for Transaction {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        writeln!(f, "Transaction {{")?;
206        for (i, call) in self.calls.iter().enumerate() {
207            writeln!(f, "  Call {i} {{")?;
208            writeln!(f, "    contract_id: {:?}", call.data.contract_id.inner())?;
209            let calldata = &call.data.data;
210            if !calldata.is_empty() {
211                writeln!(f, "    function_code: {}", calldata[0])?;
212            }
213            writeln!(f, "    parent: {:?}", call.parent_index)?;
214            writeln!(f, "    children: {:?}", call.children_indexes)?;
215            writeln!(f, "  }},")?;
216        }
217        writeln!(f, "}}")
218    }
219}
220
221#[cfg(feature = "net")]
222use crate::{
223    net::{metering::MeteringConfiguration, Message},
224    util::time::NanoTimestamp,
225};
226
227#[cfg(feature = "net")]
228// TODO: Fine tune
229// Since messages are asynchronous we will define loose rules to prevent spamming.
230// Each message score will be 1, with a threshold of 100 and expiry time of 5.
231// We are not limiting `Transaction` size.
232crate::impl_p2p_message!(
233    Transaction,
234    "tx",
235    0,
236    1,
237    MeteringConfiguration {
238        threshold: 100,
239        sleep_step: 500,
240        expiry_time: NanoTimestamp::from_secs(5),
241    }
242);
243
244/// Calls tree bounds definitions
245// TODO: increase min to 2 when fees are implement
246pub const MIN_TX_CALLS: usize = 1;
247// TODO: verify max value
248pub const MAX_TX_CALLS: usize = 20;
249
250/// Auxiliarry structure containing all the information
251/// required to execute a contract call.
252#[derive(Clone)]
253pub struct ContractCallLeaf {
254    /// Call executed
255    pub call: ContractCall,
256    /// Attached ZK proofs
257    pub proofs: Vec<Proof>,
258}
259
260/// Auxiliary structure to build a full [`Transaction`] using
261/// [`DarkTree`] to order everything.
262pub struct TransactionBuilder {
263    /// Contract calls trees forest
264    pub calls: DarkForest<ContractCallLeaf>,
265}
266
267// TODO: for now we build the trees manually, but we should
268//       add all the proper functions for easier building.
269impl TransactionBuilder {
270    /// Initialize the builder, using provided data to
271    /// generate its initial [`DarkTree`] root.
272    pub fn new(
273        data: ContractCallLeaf,
274        children: Vec<DarkTree<ContractCallLeaf>>,
275    ) -> DarkTreeResult<Self> {
276        let calls = DarkForest::new(Some(MIN_TX_CALLS), Some(MAX_TX_CALLS));
277        let mut self_ = Self { calls };
278        self_.append(data, children)?;
279        Ok(self_)
280    }
281
282    /// Append a new call tree to the forest
283    pub fn append(
284        &mut self,
285        data: ContractCallLeaf,
286        children: Vec<DarkTree<ContractCallLeaf>>,
287    ) -> DarkTreeResult<()> {
288        let tree = DarkTree::new(data, children, None, None);
289        self.calls.append(tree)
290    }
291
292    /// Builder builds the calls vector using the [`DarkForest`]
293    /// and generates the corresponding [`Transaction`].
294    pub fn build(&mut self) -> DarkTreeResult<Transaction> {
295        // Build the leafs vector
296        let leafs = self.calls.build_vec()?;
297
298        // Double check integrity
299        dark_forest_leaf_vec_integrity_check(&leafs, Some(MIN_TX_CALLS), Some(MAX_TX_CALLS))?;
300
301        // Build the corresponding transaction
302        let mut calls = Vec::with_capacity(leafs.len());
303        let mut proofs = Vec::with_capacity(leafs.len());
304        for leaf in leafs {
305            let call = DarkLeaf {
306                data: leaf.data.call,
307                parent_index: leaf.parent_index,
308                children_indexes: leaf.children_indexes,
309            };
310            calls.push(call);
311            proofs.push(leaf.data.proofs);
312        }
313
314        Ok(Transaction { calls, proofs, signatures: vec![] })
315    }
316}