fedimint_core/
transaction.rs

1use std::fmt;
2
3use bitcoin::hashes::Hash;
4use bitcoin::hex::DisplayHex as _;
5use fedimint_core::core::{DynInput, DynOutput};
6use fedimint_core::encoding::{Decodable, Encodable};
7use fedimint_core::module::SerdeModuleEncoding;
8use fedimint_core::{Amount, TransactionId};
9use thiserror::Error;
10
11use crate::config::ALEPH_BFT_UNIT_BYTE_LIMIT;
12use crate::core::{DynInputError, DynOutputError};
13
14/// An atomic value transfer operation within the Fedimint system and consensus
15///
16/// The mint enforces that the total value of the outputs equals the total value
17/// of the inputs, to prevent creating funds out of thin air. In some cases, the
18/// value of the inputs and outputs can both be 0 e.g. when creating an offer to
19/// a Lightning Gateway.
20#[derive(Debug, Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
21pub struct Transaction {
22    /// [`DynInput`]s consumed by the transaction
23    pub inputs: Vec<DynInput>,
24    /// [`DynOutput`]s created as a result of the transaction
25    pub outputs: Vec<DynOutput>,
26    /// No defined meaning, can be used to send the otherwise exactly same
27    /// transaction multiple times if the module inputs and outputs don't
28    /// introduce enough entropy.
29    ///
30    /// In the future the nonce can be used for grinding a tx hash that fulfills
31    /// certain PoW requirements.
32    pub nonce: [u8; 8],
33    /// signatures for all the public keys of the inputs
34    pub signatures: TransactionSignature,
35}
36
37pub type SerdeTransaction = SerdeModuleEncoding<Transaction>;
38
39impl Transaction {
40    /// Maximum size that a transaction can have while still fitting into an
41    /// AlephBFT unit. Subtracting 32 bytes is overly conservative, even in the
42    /// worst case the CI serialization around the transaction should never add
43    /// that much overhead. But since the byte limit is 50kb right now a few
44    /// bytes more or less won't make a difference and we can afford the safety
45    /// margin.
46    ///
47    /// A realistic value would be 7:
48    ///  * 1 byte for length of vector of CIs
49    ///  * 1 byte for the CI enum variant
50    ///  * 5 byte for the CI enum variant length
51    pub const MAX_TX_SIZE: usize = ALEPH_BFT_UNIT_BYTE_LIMIT - 32;
52
53    /// Hash of the transaction (excluding the signature).
54    ///
55    /// Transaction signature commits to this hash.
56    /// To generate it without already having a signature use
57    /// [`Self::tx_hash_from_parts`].
58    pub fn tx_hash(&self) -> TransactionId {
59        Self::tx_hash_from_parts(&self.inputs, &self.outputs, self.nonce)
60    }
61
62    /// Generate the transaction hash.
63    pub fn tx_hash_from_parts(
64        inputs: &[DynInput],
65        outputs: &[DynOutput],
66        nonce: [u8; 8],
67    ) -> TransactionId {
68        let mut engine = TransactionId::engine();
69        inputs
70            .consensus_encode(&mut engine)
71            .expect("write to hash engine can't fail");
72        outputs
73            .consensus_encode(&mut engine)
74            .expect("write to hash engine can't fail");
75        nonce
76            .consensus_encode(&mut engine)
77            .expect("write to hash engine can't fail");
78        TransactionId::from_engine(engine)
79    }
80
81    /// Validate the schnorr signatures signed over the `tx_hash`
82    pub fn validate_signatures(
83        &self,
84        pub_keys: &[secp256k1::PublicKey],
85    ) -> Result<(), TransactionError> {
86        let signatures = match &self.signatures {
87            TransactionSignature::NaiveMultisig(sigs) => sigs,
88            TransactionSignature::Default { variant, .. } => {
89                return Err(TransactionError::UnsupportedSignatureScheme { variant: *variant })
90            }
91        };
92
93        if pub_keys.len() != signatures.len() {
94            return Err(TransactionError::InvalidWitnessLength);
95        }
96
97        let txid = self.tx_hash();
98        let msg = secp256k1::Message::from_digest_slice(&txid[..]).expect("txid has right length");
99
100        for (pk, signature) in pub_keys.iter().zip(signatures) {
101            if secp256k1::global::SECP256K1
102                .verify_schnorr(signature, &msg, &pk.x_only_public_key().0)
103                .is_err()
104            {
105                return Err(TransactionError::InvalidSignature {
106                    tx: self.consensus_encode_to_hex(),
107                    hash: self.tx_hash().consensus_encode_to_hex(),
108                    sig: signature.consensus_encode_to_hex(),
109                    key: pk.consensus_encode_to_hex(),
110                });
111            }
112        }
113
114        Ok(())
115    }
116}
117
118#[derive(Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
119pub enum TransactionSignature {
120    NaiveMultisig(Vec<fedimint_core::secp256k1::schnorr::Signature>),
121    #[encodable_default]
122    Default {
123        variant: u64,
124        bytes: Vec<u8>,
125    },
126}
127
128impl fmt::Debug for TransactionSignature {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::NaiveMultisig(multi) => {
132                f.debug_struct("NaiveMultisig")
133                    .field("len", &multi.len())
134                    .finish()?;
135            }
136            Self::Default { variant, bytes } => {
137                f.debug_struct(stringify!($name))
138                    .field("variant", variant)
139                    .field("bytes", &bytes.as_hex())
140                    .finish()?;
141            }
142        }
143        Ok(())
144    }
145}
146
147#[derive(Debug, Error, Encodable, Decodable, Clone, Eq, PartialEq)]
148pub enum TransactionError {
149    #[error("The transaction is unbalanced (in={inputs}, out={outputs}, fee={fee})")]
150    UnbalancedTransaction {
151        inputs: Amount,
152        outputs: Amount,
153        fee: Amount,
154    },
155    #[error("The transaction's signature is invalid: tx={tx}, hash={hash}, sig={sig}, key={key}")]
156    InvalidSignature {
157        tx: String,
158        hash: String,
159        sig: String,
160        key: String,
161    },
162    #[error("The transaction's signature scheme is not supported: variant={variant}")]
163    UnsupportedSignatureScheme { variant: u64 },
164    #[error("The transaction did not have the correct number of signatures")]
165    InvalidWitnessLength,
166    #[error("The transaction had an invalid input: {}", .0)]
167    Input(DynInputError),
168    #[error("The transaction had an invalid output: {}", .0)]
169    Output(DynOutputError),
170}
171
172/// The transaction caused an overflow.
173///
174/// We can't add a new variant to transaction errors, so we define a special
175/// case for the retroactively added overflow error type. In a second iteration
176/// of the transaction submission API this should become a separate error
177/// variant.
178pub const TRANSACTION_OVERFLOW_ERROR: TransactionError = TransactionError::UnbalancedTransaction {
179    inputs: Amount::ZERO,
180    outputs: Amount::ZERO,
181    fee: Amount::ZERO,
182};
183
184#[derive(Debug, Encodable, Decodable, Clone, Eq, PartialEq)]
185pub struct TransactionSubmissionOutcome(pub Result<TransactionId, TransactionError>);