Skip to main content

fedimint_client_module/transaction/
builder.rs

1use std::fmt;
2use std::ops::RangeInclusive;
3use std::sync::Arc;
4
5use bitcoin::key::Keypair;
6use bitcoin::secp256k1;
7use fedimint_core::core::{
8    DynInput, DynOutput, IInput, IOutput, IntoDynInstance, ModuleInstanceId,
9};
10use fedimint_core::encoding::{Decodable, Encodable};
11use fedimint_core::module::Amounts;
12use fedimint_core::task::{MaybeSend, MaybeSync};
13use fedimint_core::transaction::{Transaction, TransactionSignature};
14use fedimint_logging::LOG_CLIENT;
15use itertools::multiunzip;
16use rand::{CryptoRng, Rng, RngCore};
17use secp256k1::Secp256k1;
18use tracing::warn;
19
20use crate::module::{IdxRange, OutPointRange, StateGenerator};
21use crate::sm::{self, DynState};
22use crate::{
23    InstancelessDynClientInput, InstancelessDynClientInputBundle, InstancelessDynClientInputSM,
24    InstancelessDynClientOutput, InstancelessDynClientOutputBundle, InstancelessDynClientOutputSM,
25    states_add_instance, states_to_instanceless_dyn,
26};
27
28#[derive(Clone, Debug)]
29pub struct ClientInput<I = DynInput> {
30    pub input: I,
31    pub keys: Vec<Keypair>,
32    pub amounts: Amounts,
33}
34
35#[derive(Clone)]
36pub struct ClientInputSM<S = DynState> {
37    pub state_machines: StateGenerator<S>,
38}
39
40impl<S> fmt::Debug for ClientInputSM<S> {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.write_str("ClientInputSM")
43    }
44}
45
46/// A fake [`sm::Context`] for [`NeverClientStateMachine`]
47#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
48pub enum NeverClientContext {}
49
50impl sm::Context for NeverClientContext {
51    const KIND: Option<fedimint_core::core::ModuleKind> = None;
52}
53
54/// A fake [`sm::State`] that can actually never happen.
55///
56/// Useful as a default for type inference in cases where there are no
57/// state machines involved in [`ClientInputBundle`].
58#[derive(Debug, Clone, Eq, PartialEq, Hash, Decodable, Encodable)]
59pub enum NeverClientStateMachine {}
60
61impl IntoDynInstance for NeverClientStateMachine {
62    type DynType = DynState;
63
64    fn into_dyn(self, _instance_id: ModuleInstanceId) -> Self::DynType {
65        unreachable!()
66    }
67}
68impl sm::State for NeverClientStateMachine {
69    type ModuleContext = NeverClientContext;
70
71    fn transitions(
72        &self,
73        _context: &Self::ModuleContext,
74        _global_context: &crate::DynGlobalClientContext,
75    ) -> Vec<sm::StateTransition<Self>> {
76        unreachable!()
77    }
78
79    fn operation_id(&self) -> fedimint_core::core::OperationId {
80        unreachable!()
81    }
82}
83
84/// A group of inputs and state machines responsible for driving their state
85///
86/// These must be kept together as a whole when including in a transaction.
87#[derive(Clone, Debug)]
88pub struct ClientInputBundle<I = DynInput, S = DynState> {
89    pub(crate) inputs: Vec<ClientInput<I>>,
90    pub(crate) sm_gens: Vec<ClientInputSM<S>>,
91}
92
93impl<I> ClientInputBundle<I, NeverClientStateMachine> {
94    /// A version of [`Self::new`] for times where input does not require any
95    /// state machines
96    ///
97    /// This avoids type inference issues of `S`, and saves some typing.
98    pub fn new_no_sm(inputs: Vec<ClientInput<I>>) -> Self {
99        if inputs.is_empty() {
100            // TODO: Make it return Result or assert?
101            warn!(target: LOG_CLIENT, "Empty input bundle will be illegal in the future");
102        }
103        Self {
104            inputs,
105            sm_gens: vec![],
106        }
107    }
108}
109
110impl<I, S> ClientInputBundle<I, S>
111where
112    I: IInput + MaybeSend + MaybeSync + 'static,
113    S: sm::IState + MaybeSend + MaybeSync + 'static,
114{
115    pub fn new(inputs: Vec<ClientInput<I>>, sm_gens: Vec<ClientInputSM<S>>) -> Self {
116        Self { inputs, sm_gens }
117    }
118
119    pub fn sms(&self) -> &[ClientInputSM<S>] {
120        &self.sm_gens
121    }
122
123    pub fn into_instanceless(self) -> InstancelessDynClientInputBundle {
124        InstancelessDynClientInputBundle {
125            inputs: self
126                .inputs
127                .into_iter()
128                .map(|input| InstancelessDynClientInput {
129                    input: Box::new(input.input),
130                    keys: input.keys,
131                    amounts: input.amounts,
132                })
133                .collect(),
134            sm_gens: self
135                .sm_gens
136                .into_iter()
137                .map(|input_sm| InstancelessDynClientInputSM {
138                    state_machines: states_to_instanceless_dyn(input_sm.state_machines),
139                })
140                .collect(),
141        }
142    }
143}
144
145impl<I, S> ClientInputBundle<I, S> {
146    pub fn inputs(&self) -> &[ClientInput<I>] {
147        &self.inputs
148    }
149
150    pub fn is_empty(&self) -> bool {
151        // Notably, sm_gen will not be called when inputs are empty anyway
152        self.inputs.is_empty()
153    }
154}
155
156impl<I> IntoDynInstance for ClientInput<I>
157where
158    I: IntoDynInstance<DynType = DynInput> + 'static,
159{
160    type DynType = ClientInput;
161
162    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInput {
163        ClientInput {
164            input: self.input.into_dyn(module_instance_id),
165            keys: self.keys,
166            amounts: self.amounts,
167        }
168    }
169}
170
171impl<S> IntoDynInstance for ClientInputSM<S>
172where
173    S: IntoDynInstance<DynType = DynState> + 'static,
174{
175    type DynType = ClientInputSM;
176
177    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputSM {
178        ClientInputSM {
179            state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
180        }
181    }
182}
183
184impl<I, S> IntoDynInstance for ClientInputBundle<I, S>
185where
186    I: IntoDynInstance<DynType = DynInput> + 'static,
187    S: IntoDynInstance<DynType = DynState> + 'static,
188{
189    type DynType = ClientInputBundle;
190
191    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
192        ClientInputBundle {
193            inputs: self
194                .inputs
195                .into_iter()
196                .map(|input| input.into_dyn(module_instance_id))
197                .collect::<Vec<ClientInput>>(),
198
199            sm_gens: self
200                .sm_gens
201                .into_iter()
202                .map(|input_sm| input_sm.into_dyn(module_instance_id))
203                .collect::<Vec<ClientInputSM>>(),
204        }
205    }
206}
207
208impl IntoDynInstance for InstancelessDynClientInputBundle {
209    type DynType = ClientInputBundle;
210
211    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientInputBundle {
212        ClientInputBundle {
213            inputs: self
214                .inputs
215                .into_iter()
216                .map(|input| ClientInput {
217                    input: DynInput::from_parts(module_instance_id, input.input),
218                    keys: input.keys,
219                    amounts: input.amounts,
220                })
221                .collect::<Vec<ClientInput>>(),
222
223            sm_gens: self
224                .sm_gens
225                .into_iter()
226                .map(|input_sm| ClientInputSM {
227                    state_machines: states_add_instance(
228                        module_instance_id,
229                        input_sm.state_machines,
230                    ),
231                })
232                .collect::<Vec<ClientInputSM>>(),
233        }
234    }
235}
236
237#[derive(Clone, Debug)]
238pub struct ClientOutputBundle<O = DynOutput, S = DynState> {
239    pub(crate) outputs: Vec<ClientOutput<O>>,
240    pub(crate) sm_gens: Vec<ClientOutputSM<S>>,
241}
242
243#[derive(Clone, Debug)]
244pub struct ClientOutput<O = DynOutput> {
245    pub output: O,
246    pub amounts: Amounts,
247}
248
249#[derive(Clone)]
250pub struct ClientOutputSM<S = DynState> {
251    pub state_machines: StateGenerator<S>,
252}
253
254impl<S> fmt::Debug for ClientOutputSM<S> {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        f.write_str("ClientOutputSM")
257    }
258}
259impl<O> ClientOutputBundle<O, NeverClientStateMachine> {
260    /// A version of [`Self::new`] for times where output does not require any
261    /// state machines
262    ///
263    /// This avoids type inference issues of `S`, and saves some typing.
264    pub fn new_no_sm(outputs: Vec<ClientOutput<O>>) -> Self {
265        if outputs.is_empty() {
266            // TODO: Make it return Result or assert?
267            warn!(target: LOG_CLIENT, "Empty output bundle will be illegal in the future");
268        }
269        Self {
270            outputs,
271            sm_gens: vec![],
272        }
273    }
274}
275impl<O, S> ClientOutputBundle<O, S> {
276    pub fn outputs(&self) -> &[ClientOutput<O>] {
277        &self.outputs
278    }
279}
280
281impl<O, S> ClientOutputBundle<O, S>
282where
283    O: IOutput + MaybeSend + MaybeSync + 'static,
284    S: sm::IState + MaybeSend + MaybeSync + 'static,
285{
286    pub fn new(outputs: Vec<ClientOutput<O>>, sm_gens: Vec<ClientOutputSM<S>>) -> Self {
287        Self { outputs, sm_gens }
288    }
289
290    pub fn sms(&self) -> &[ClientOutputSM<S>] {
291        &self.sm_gens
292    }
293
294    pub fn with(mut self, other: Self) -> Self {
295        self.outputs.extend(other.outputs);
296        self.sm_gens.extend(other.sm_gens);
297        self
298    }
299
300    pub fn into_instanceless(self) -> InstancelessDynClientOutputBundle {
301        InstancelessDynClientOutputBundle {
302            outputs: self
303                .outputs
304                .into_iter()
305                .map(|output| InstancelessDynClientOutput {
306                    output: Box::new(output.output),
307                    amounts: output.amounts,
308                })
309                .collect(),
310            sm_gens: self
311                .sm_gens
312                .into_iter()
313                .map(|output_sm| InstancelessDynClientOutputSM {
314                    state_machines: states_to_instanceless_dyn(output_sm.state_machines),
315                })
316                .collect(),
317        }
318    }
319}
320
321impl<O, S> ClientOutputBundle<O, S> {
322    pub fn is_empty(&self) -> bool {
323        // Notably, sm_gen will not be called when outputs are empty anyway
324        self.outputs.is_empty()
325    }
326}
327
328impl<I, S> IntoDynInstance for ClientOutputBundle<I, S>
329where
330    I: IntoDynInstance<DynType = DynOutput> + 'static,
331    S: IntoDynInstance<DynType = DynState> + 'static,
332{
333    type DynType = ClientOutputBundle;
334
335    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
336        ClientOutputBundle {
337            outputs: self
338                .outputs
339                .into_iter()
340                .map(|output| output.into_dyn(module_instance_id))
341                .collect::<Vec<ClientOutput>>(),
342
343            sm_gens: self
344                .sm_gens
345                .into_iter()
346                .map(|output_sm| output_sm.into_dyn(module_instance_id))
347                .collect::<Vec<ClientOutputSM>>(),
348        }
349    }
350}
351
352impl IntoDynInstance for InstancelessDynClientOutputBundle {
353    type DynType = ClientOutputBundle;
354
355    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputBundle {
356        ClientOutputBundle {
357            outputs: self
358                .outputs
359                .into_iter()
360                .map(|output| ClientOutput {
361                    output: DynOutput::from_parts(module_instance_id, output.output),
362                    amounts: output.amounts,
363                })
364                .collect::<Vec<ClientOutput>>(),
365
366            sm_gens: self
367                .sm_gens
368                .into_iter()
369                .map(|output_sm| ClientOutputSM {
370                    state_machines: states_add_instance(
371                        module_instance_id,
372                        output_sm.state_machines,
373                    ),
374                })
375                .collect::<Vec<ClientOutputSM>>(),
376        }
377    }
378}
379
380impl<I> IntoDynInstance for ClientOutput<I>
381where
382    I: IntoDynInstance<DynType = DynOutput> + 'static,
383{
384    type DynType = ClientOutput;
385
386    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutput {
387        ClientOutput {
388            output: self.output.into_dyn(module_instance_id),
389            amounts: self.amounts,
390        }
391    }
392}
393
394impl<S> IntoDynInstance for ClientOutputSM<S>
395where
396    S: IntoDynInstance<DynType = DynState> + 'static,
397{
398    type DynType = ClientOutputSM;
399
400    fn into_dyn(self, module_instance_id: ModuleInstanceId) -> ClientOutputSM {
401        ClientOutputSM {
402            state_machines: state_gen_to_dyn(self.state_machines, module_instance_id),
403        }
404    }
405}
406
407/// The explicit (operation-supplied) side of a transaction, summarized for a
408/// fee quote via `Client::fee_quote`.
409///
410/// A module describes the inputs and outputs its operation would contribute by
411/// their gross value and federation fees (per unit), rather than building the
412/// actual transaction. The primary module for each affected unit then balances
413/// the resulting imbalance with change, and the full breakdown is returned as a
414/// [`FeeQuote`]. This avoids fabricating real inputs/outputs (which may require
415/// cryptographic material not available when quoting), since fees depend only
416/// on amounts and module.
417///
418/// All four fields are multi-unit [`Amounts`], so an operation can describe
419/// explicit items spanning several units (e.g. Bitcoin plus a custom currency);
420/// each unit is then balanced independently by its own primary module, the same
421/// way `Client::finalize_transaction` does.
422///
423/// For a typical receive there are no explicit outputs, so `output_amount` and
424/// `output_fee` are [`Amounts::ZERO`].
425#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct FeeQuoteRequest {
427    /// Gross value of the operation's explicit inputs, per unit.
428    pub input_amount: Amounts,
429    /// Gross value of the operation's explicit outputs, per unit.
430    pub output_amount: Amounts,
431    /// Federation fees charged on the operation's explicit inputs, per unit.
432    pub input_fee: Amounts,
433    /// Federation fees charged on the operation's explicit outputs, per unit.
434    pub output_fee: Amounts,
435}
436
437/// Breakdown of the fee finalizing a transaction would incur, as computed by
438/// `Client::fee_quote` (a dry-run of the same balancing the real submission
439/// performs).
440///
441/// This is module-agnostic: the explicit inputs/outputs are described by the
442/// [`FeeQuoteRequest`] from whichever module is quoting (mint, lightning,
443/// wallet, …) and the change is generated by the primary module. The quote is
444/// point-in-time: it depends on the client's current inventory and can move as
445/// funds change.
446///
447/// Each field is a multi-unit [`Amounts`], since the quoted operation may span
448/// several units. The total fee is the sum of the breakdown fields, available
449/// via [`FeeQuote::total`].
450#[derive(Debug, Clone, PartialEq, Eq)]
451pub struct FeeQuote {
452    /// Federation fees charged on the spent (input) items — both the
453    /// transaction's explicit inputs and any inputs the primary module pulls in
454    /// to balance it.
455    pub input: Amounts,
456    /// Federation fees charged on the created (output) items — both the
457    /// transaction's explicit outputs and the change minted to balance it.
458    pub output: Amounts,
459    /// Sub-denomination remainder that cannot form an output and is lost.
460    pub dust: Amounts,
461}
462
463impl FeeQuote {
464    /// A zero fee, for operations that incur no cost at all — e.g. an ecash
465    /// send served entirely from existing exact-change notes, which submits
466    /// no transaction.
467    pub const ZERO: Self = Self {
468        input: Amounts::ZERO,
469        output: Amounts::ZERO,
470        dust: Amounts::ZERO,
471    };
472
473    /// Total fee per unit: everything the gross input value does not become a
474    /// net wallet gain. Equal to `input + output + dust`.
475    pub fn total(&self) -> Amounts {
476        self.input
477            .clone()
478            .checked_add(&self.output)
479            .and_then(|sum| sum.checked_add(&self.dust))
480            .expect("aggregate fee components cannot overflow an Amounts")
481    }
482}
483
484#[derive(Default, Clone, Debug)]
485pub struct TransactionBuilder {
486    inputs: Vec<ClientInputBundle>,
487    outputs: Vec<ClientOutputBundle>,
488}
489
490impl TransactionBuilder {
491    pub fn new() -> Self {
492        Self::default()
493    }
494
495    pub fn with_inputs(mut self, inputs: ClientInputBundle) -> Self {
496        self.inputs.push(inputs);
497        self
498    }
499
500    pub fn with_outputs(mut self, outputs: ClientOutputBundle) -> Self {
501        self.outputs.push(outputs);
502        self
503    }
504
505    pub fn build<C, R: RngCore + CryptoRng>(
506        self,
507        secp_ctx: &Secp256k1<C>,
508        mut rng: R,
509    ) -> (Transaction, Vec<DynState>)
510    where
511        C: secp256k1::Signing + secp256k1::Verification,
512    {
513        // `input_idx_to_bundle_idx[input_idx]` stores the index of a bundle the input
514        // at `input_idx` comes from, so we can call state machines of the
515        // corresponding bundle for every input bundle. It is always
516        // monotonically increasing, e.g. `[0, 0, 1, 2, 2, 2, 4]`
517        let (input_idx_to_bundle_idx, inputs, input_keys): (Vec<_>, Vec<_>, Vec<_>) = multiunzip(
518            self.inputs
519                .iter()
520                .enumerate()
521                .flat_map(|(bundle_idx, bundle)| {
522                    bundle
523                        .inputs
524                        .iter()
525                        .map(move |input| (bundle_idx, input.input.clone(), input.keys.clone()))
526                }),
527        );
528        // `output_idx_to_bundle` works exactly like `input_idx_to_bundle_idx` above,
529        // but for outputs.
530        let (output_idx_to_bundle_idx, outputs): (Vec<_>, Vec<_>) = multiunzip(
531            self.outputs
532                .iter()
533                .enumerate()
534                .flat_map(|(bundle_idx, bundle)| {
535                    bundle
536                        .outputs
537                        .iter()
538                        .map(move |output| (bundle_idx, output.output.clone()))
539                }),
540        );
541        let nonce: [u8; 8] = rng.r#gen();
542
543        let txid = Transaction::tx_hash_from_parts(&inputs, &outputs, nonce);
544        let msg = secp256k1::Message::from_digest_slice(&txid[..]).expect("txid has right length");
545
546        let signatures = input_keys
547            .iter()
548            .flatten()
549            .map(|keypair| secp_ctx.sign_schnorr(&msg, keypair))
550            .collect();
551
552        let transaction = Transaction {
553            inputs,
554            outputs,
555            nonce,
556            signatures: TransactionSignature::NaiveMultisig(signatures),
557        };
558
559        let input_states = self
560            .inputs
561            .into_iter()
562            .enumerate()
563            .filter(|(_, bundle)| !bundle.is_empty())
564            .flat_map(|(bundle_idx, bundle)| {
565                let input_idxs = find_range_of_matching_items(&input_idx_to_bundle_idx, bundle_idx)
566                    .expect("Non empty bundles must always have a match");
567                bundle.sm_gens.into_iter().flat_map(move |sm| {
568                    (sm.state_machines)(OutPointRange::new(
569                        txid,
570                        IdxRange::from_inclusive(input_idxs.clone()).expect("can't overflow"),
571                    ))
572                })
573            });
574
575        let output_states = self
576            .outputs
577            .into_iter()
578            .enumerate()
579            .filter(|(_, bundle)| !bundle.is_empty())
580            .flat_map(|(bundle_idx, bundle)| {
581                let output_idxs =
582                    find_range_of_matching_items(&output_idx_to_bundle_idx, bundle_idx)
583                        .expect("Non empty bundles must always have a match");
584                bundle.sm_gens.into_iter().flat_map(move |sm| {
585                    (sm.state_machines)(OutPointRange::new(
586                        txid,
587                        IdxRange::from_inclusive(output_idxs.clone())
588                            .expect("can't possibly overflow"),
589                    ))
590                })
591            });
592        (transaction, input_states.chain(output_states).collect())
593    }
594
595    pub fn inputs(&self) -> impl Iterator<Item = &ClientInput> {
596        self.inputs.iter().flat_map(|i| i.inputs.iter())
597    }
598
599    pub fn outputs(&self) -> impl Iterator<Item = &ClientOutput> {
600        self.outputs.iter().flat_map(|i| i.outputs.iter())
601    }
602}
603
604/// Find the range of indexes in an monotonically increasing `arr`, that is
605/// equal to `item`
606fn find_range_of_matching_items(arr: &[usize], item: usize) -> Option<RangeInclusive<u64>> {
607    // `arr` must be monotonically increasing
608    debug_assert!(arr.windows(2).all(|w| w[0] <= w[1]));
609
610    arr.iter()
611        .enumerate()
612        .filter_map(|(arr_idx, arr_item)| (*arr_item == item).then_some(arr_idx as u64))
613        .fold(None, |cur: Option<(u64, u64)>, idx| {
614            Some(cur.map_or((idx, idx), |cur| (cur.0.min(idx), cur.1.max(idx))))
615        })
616        .map(|(start, end)| start..=end)
617}
618
619#[test]
620fn find_range_of_matching_items_sanity() {
621    assert_eq!(find_range_of_matching_items(&[0, 0], 0), Some(0..=1));
622    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 0), Some(0..=1));
623    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 1), Some(2..=2));
624    assert_eq!(find_range_of_matching_items(&[0, 0, 1], 2), None);
625    assert_eq!(find_range_of_matching_items(&[], 0), None);
626}
627
628fn state_gen_to_dyn<S>(
629    state_gen: StateGenerator<S>,
630    module_instance: ModuleInstanceId,
631) -> StateGenerator<DynState>
632where
633    S: IntoDynInstance<DynType = DynState> + 'static,
634{
635    Arc::new(move |out_point_range| {
636        let states = state_gen(out_point_range);
637        states
638            .into_iter()
639            .map(|state| state.into_dyn(module_instance))
640            .collect()
641    })
642}
643
644#[cfg(test)]
645mod tests;