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