fedimint_server/consensus/
transaction.rs

1use fedimint_core::db::DatabaseTransaction;
2use fedimint_core::module::{CoreConsensusVersion, TransactionItemAmount};
3use fedimint_core::transaction::{TRANSACTION_OVERFLOW_ERROR, Transaction, TransactionError};
4use fedimint_core::{Amount, InPoint, OutPoint};
5use fedimint_server_core::ServerModuleRegistry;
6use rayon::iter::{IntoParallelIterator, ParallelIterator};
7
8use crate::metrics::{CONSENSUS_TX_PROCESSED_INPUTS, CONSENSUS_TX_PROCESSED_OUTPUTS};
9
10#[derive(Debug, PartialEq, Eq)]
11pub enum TxProcessingMode {
12    Submission,
13    Consensus,
14}
15
16pub async fn process_transaction_with_dbtx(
17    modules: ServerModuleRegistry,
18    dbtx: &mut DatabaseTransaction<'_>,
19    transaction: &Transaction,
20    version: CoreConsensusVersion,
21    mode: TxProcessingMode,
22) -> Result<(), TransactionError> {
23    let in_count = transaction.inputs.len();
24    let out_count = transaction.outputs.len();
25
26    dbtx.on_commit(move || {
27        CONSENSUS_TX_PROCESSED_INPUTS.observe(in_count as f64);
28        CONSENSUS_TX_PROCESSED_OUTPUTS.observe(out_count as f64);
29    });
30
31    // We can not return the error here as errors are not returned in a specified
32    // order and the client still expects consensus on the error. Since the
33    // error is not extensible at the moment we need to incorrectly return the
34    // InvalidWitnessLength variant.
35    transaction
36        .inputs
37        .clone()
38        .into_par_iter()
39        .try_for_each(|input| {
40            modules
41                .get_expect(input.module_instance_id())
42                .verify_input(&input)
43        })
44        .map_err(|_| TransactionError::InvalidWitnessLength)?;
45
46    let mut funding_verifier = FundingVerifier::default();
47    let mut public_keys = Vec::new();
48
49    let txid = transaction.tx_hash();
50
51    for (input, in_idx) in transaction.inputs.iter().zip(0u64..) {
52        // somewhat unfortunately, we need to do the extra checks berofe `process_x`
53        // does the changes in the dbtx
54        if mode == TxProcessingMode::Submission {
55            modules
56                .get_expect(input.module_instance_id())
57                .verify_input_submission(
58                    &mut dbtx
59                        .to_ref_with_prefix_module_id(input.module_instance_id())
60                        .0,
61                    input,
62                )
63                .await
64                .map_err(TransactionError::Input)?;
65        }
66        let meta = modules
67            .get_expect(input.module_instance_id())
68            .process_input(
69                &mut dbtx
70                    .to_ref_with_prefix_module_id(input.module_instance_id())
71                    .0,
72                input,
73                InPoint { txid, in_idx },
74            )
75            .await
76            .map_err(TransactionError::Input)?;
77
78        funding_verifier.add_input(meta.amount)?;
79        public_keys.push(meta.pub_key);
80    }
81
82    transaction.validate_signatures(&public_keys)?;
83
84    for (output, out_idx) in transaction.outputs.iter().zip(0u64..) {
85        // somewhat unfortunately, we need to do the extra checks berofe `process_x`
86        // does the changes in the dbtx
87        if mode == TxProcessingMode::Submission {
88            modules
89                .get_expect(output.module_instance_id())
90                .verify_output_submission(
91                    &mut dbtx
92                        .to_ref_with_prefix_module_id(output.module_instance_id())
93                        .0,
94                    output,
95                    OutPoint { txid, out_idx },
96                )
97                .await
98                .map_err(TransactionError::Output)?;
99        }
100
101        let amount = modules
102            .get_expect(output.module_instance_id())
103            .process_output(
104                &mut dbtx
105                    .to_ref_with_prefix_module_id(output.module_instance_id())
106                    .0,
107                output,
108                OutPoint { txid, out_idx },
109            )
110            .await
111            .map_err(TransactionError::Output)?;
112
113        funding_verifier.add_output(amount)?;
114    }
115
116    funding_verifier.verify_funding(version)?;
117
118    Ok(())
119}
120
121#[allow(clippy::struct_field_names)]
122pub struct FundingVerifier {
123    input_amount: Amount,
124    output_amount: Amount,
125    fee_amount: Amount,
126}
127
128impl FundingVerifier {
129    pub fn add_input(
130        &mut self,
131        input_amount: TransactionItemAmount,
132    ) -> Result<(), TransactionError> {
133        self.input_amount = self
134            .input_amount
135            .checked_add(input_amount.amount)
136            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
137
138        self.fee_amount = self
139            .fee_amount
140            .checked_add(input_amount.fee)
141            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
142
143        Ok(())
144    }
145
146    pub fn add_output(
147        &mut self,
148        output_amount: TransactionItemAmount,
149    ) -> Result<(), TransactionError> {
150        self.output_amount = self
151            .output_amount
152            .checked_add(output_amount.amount)
153            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
154
155        self.fee_amount = self
156            .fee_amount
157            .checked_add(output_amount.fee)
158            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
159
160        Ok(())
161    }
162
163    pub fn verify_funding(self, version: CoreConsensusVersion) -> Result<(), TransactionError> {
164        let outputs_and_fees = self
165            .output_amount
166            .checked_add(self.fee_amount)
167            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
168
169        if self.input_amount == outputs_and_fees {
170            return Ok(());
171        }
172
173        if self.input_amount > outputs_and_fees && version >= CoreConsensusVersion::new(2, 1) {
174            return Ok(());
175        }
176
177        Err(TransactionError::UnbalancedTransaction {
178            inputs: self.input_amount,
179            outputs: self.output_amount,
180            fee: self.fee_amount,
181        })
182    }
183}
184
185impl Default for FundingVerifier {
186    fn default() -> Self {
187        FundingVerifier {
188            input_amount: Amount::ZERO,
189            output_amount: Amount::ZERO,
190            fee_amount: Amount::ZERO,
191        }
192    }
193}