Skip to main content

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(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
37impl fmt::Debug for Transaction {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.debug_struct("Transaction")
40            .field("txid", &self.tx_hash())
41            .field("inputs", &self.inputs)
42            .field("outputs", &self.outputs)
43            .field("nonce", &self.nonce)
44            .field("signatures", &self.signatures)
45            .finish()
46    }
47}
48
49pub type SerdeTransaction = SerdeModuleEncoding<Transaction>;
50
51impl Transaction {
52    /// Maximum size that a transaction can have while still fitting into an
53    /// AlephBFT unit. Subtracting 32 bytes is overly conservative, even in the
54    /// worst case the CI serialization around the transaction should never add
55    /// that much overhead. But since the byte limit is 50kb right now a few
56    /// bytes more or less won't make a difference and we can afford the safety
57    /// margin.
58    ///
59    /// A realistic value would be 7:
60    ///  * 1 byte for length of vector of CIs
61    ///  * 1 byte for the CI enum variant
62    ///  * 5 byte for the CI enum variant length
63    pub const MAX_TX_SIZE: usize = ALEPH_BFT_UNIT_BYTE_LIMIT - 32;
64
65    /// Hash of the transaction (excluding the signature).
66    ///
67    /// Transaction signature commits to this hash.
68    /// To generate it without already having a signature use
69    /// [`Self::tx_hash_from_parts`].
70    pub fn tx_hash(&self) -> TransactionId {
71        Self::tx_hash_from_parts(&self.inputs, &self.outputs, self.nonce)
72    }
73
74    /// Generate the transaction hash.
75    pub fn tx_hash_from_parts(
76        inputs: &[DynInput],
77        outputs: &[DynOutput],
78        nonce: [u8; 8],
79    ) -> TransactionId {
80        let mut engine = TransactionId::engine();
81        inputs
82            .consensus_encode(&mut engine)
83            .expect("write to hash engine can't fail");
84        outputs
85            .consensus_encode(&mut engine)
86            .expect("write to hash engine can't fail");
87        nonce
88            .consensus_encode(&mut engine)
89            .expect("write to hash engine can't fail");
90        TransactionId::from_engine(engine)
91    }
92
93    /// Validate the schnorr signatures signed over the `tx_hash`
94    pub fn validate_signatures(
95        &self,
96        pub_keys: &[secp256k1::PublicKey],
97    ) -> Result<(), TransactionError> {
98        let signatures = match &self.signatures {
99            TransactionSignature::NaiveMultisig(sigs) => sigs,
100            TransactionSignature::Default { variant, .. } => {
101                return Err(TransactionError::UnsupportedSignatureScheme { variant: *variant });
102            }
103        };
104
105        if pub_keys.len() != signatures.len() {
106            return Err(TransactionError::InvalidWitnessLength);
107        }
108
109        let txid = self.tx_hash();
110        let msg = secp256k1::Message::from_digest_slice(&txid[..]).expect("txid has right length");
111
112        for (pk, signature) in pub_keys.iter().zip(signatures) {
113            if secp256k1::global::SECP256K1
114                .verify_schnorr(signature, &msg, &pk.x_only_public_key().0)
115                .is_err()
116            {
117                return Err(TransactionError::InvalidSignature {
118                    tx: self.consensus_encode_to_hex(),
119                    hash: self.tx_hash().consensus_encode_to_hex(),
120                    sig: signature.consensus_encode_to_hex(),
121                    key: pk.consensus_encode_to_hex(),
122                });
123            }
124        }
125
126        Ok(())
127    }
128}
129
130#[derive(Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
131pub enum TransactionSignature {
132    NaiveMultisig(Vec<fedimint_core::secp256k1::schnorr::Signature>),
133    #[encodable_default]
134    Default {
135        variant: u64,
136        bytes: Vec<u8>,
137    },
138}
139
140impl fmt::Debug for TransactionSignature {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            Self::NaiveMultisig(multi) => {
144                f.debug_struct("NaiveMultisig")
145                    .field("len", &multi.len())
146                    .finish()?;
147            }
148            Self::Default { variant, bytes } => {
149                f.debug_struct(stringify!($name))
150                    .field("variant", variant)
151                    .field("bytes", &bytes.as_hex())
152                    .finish()?;
153            }
154        }
155        Ok(())
156    }
157}
158
159#[derive(Debug, Error, Encodable, Decodable, Clone, Eq, PartialEq)]
160pub enum TransactionError {
161    /// Transaction was not balanced
162    ///
163    /// Note: since this type existed before multi-unit amounts were implemented
164    /// and can't change shape, the unit of the imbalance is not specified.
165    #[error("The transaction is unbalanced (in={inputs}, out={outputs}, fee={fee})")]
166    UnbalancedTransaction {
167        inputs: Amount,
168        outputs: Amount,
169        fee: Amount,
170    },
171    #[error("The transaction's signature is invalid: tx={tx}, hash={hash}, sig={sig}, key={key}")]
172    InvalidSignature {
173        tx: String,
174        hash: String,
175        sig: String,
176        key: String,
177    },
178    #[error("The transaction's signature scheme is not supported: variant={variant}")]
179    UnsupportedSignatureScheme { variant: u64 },
180    #[error("The transaction did not have the correct number of signatures")]
181    InvalidWitnessLength,
182    #[error("The transaction had an invalid input: {}", .0)]
183    Input(DynInputError),
184    #[error("The transaction had an invalid output: {}", .0)]
185    Output(DynOutputError),
186}
187
188/// The transaction caused an overflow.
189///
190/// We can't add a new variant to transaction errors, so we define a special
191/// case for the retroactively added overflow error type. In a second iteration
192/// of the transaction submission API this should become a separate error
193/// variant.
194pub const TRANSACTION_OVERFLOW_ERROR: TransactionError = TransactionError::UnbalancedTransaction {
195    inputs: Amount::ZERO,
196    outputs: Amount::ZERO,
197    fee: Amount::ZERO,
198};
199
200#[derive(Debug, Encodable, Decodable, Clone, Eq, PartialEq)]
201pub struct TransactionSubmissionOutcome(pub Result<TransactionId, TransactionError>);