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
121pub struct FundingVerifier {
122    input_amount: Amount,
123    output_amount: Amount,
124    fee_amount: Amount,
125}
126
127impl FundingVerifier {
128    pub fn add_input(
129        &mut self,
130        input_amount: TransactionItemAmount,
131    ) -> Result<(), TransactionError> {
132        self.input_amount = self
133            .input_amount
134            .checked_add(input_amount.amount)
135            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
136
137        self.fee_amount = self
138            .fee_amount
139            .checked_add(input_amount.fee)
140            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
141
142        Ok(())
143    }
144
145    pub fn add_output(
146        &mut self,
147        output_amount: TransactionItemAmount,
148    ) -> Result<(), TransactionError> {
149        self.output_amount = self
150            .output_amount
151            .checked_add(output_amount.amount)
152            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
153
154        self.fee_amount = self
155            .fee_amount
156            .checked_add(output_amount.fee)
157            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
158
159        Ok(())
160    }
161
162    pub fn verify_funding(self, version: CoreConsensusVersion) -> Result<(), TransactionError> {
163        let outputs_and_fees = self
164            .output_amount
165            .checked_add(self.fee_amount)
166            .ok_or(TRANSACTION_OVERFLOW_ERROR)?;
167
168        if self.input_amount == outputs_and_fees {
169            return Ok(());
170        }
171
172        if self.input_amount > outputs_and_fees && version >= CoreConsensusVersion::new(2, 1) {
173            return Ok(());
174        }
175
176        Err(TransactionError::UnbalancedTransaction {
177            inputs: self.input_amount,
178            outputs: self.output_amount,
179            fee: self.fee_amount,
180        })
181    }
182}
183
184impl Default for FundingVerifier {
185    fn default() -> Self {
186        FundingVerifier {
187            input_amount: Amount::ZERO,
188            output_amount: Amount::ZERO,
189            fee_amount: Amount::ZERO,
190        }
191    }
192}