fedimint_core/
transaction.rs1use 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#[derive(Clone, Eq, PartialEq, Hash, Encodable, Decodable)]
21pub struct Transaction {
22 pub inputs: Vec<DynInput>,
24 pub outputs: Vec<DynOutput>,
26 pub nonce: [u8; 8],
33 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 pub const MAX_TX_SIZE: usize = ALEPH_BFT_UNIT_BYTE_LIMIT - 32;
64
65 pub fn tx_hash(&self) -> TransactionId {
71 Self::tx_hash_from_parts(&self.inputs, &self.outputs, self.nonce)
72 }
73
74 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 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 #[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
188pub 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>);