Skip to main content

fedimint_lnv2_common/
contracts.rs

1use bitcoin::hashes::sha256;
2use bitcoin::secp256k1;
3use fedimint_core::Amount;
4use fedimint_core::encoding::{Decodable, Encodable};
5use secp256k1::schnorr::Signature;
6use secp256k1::{Message, PublicKey, SecretKey};
7use serde::{Deserialize, Serialize};
8use tpe::{
9    AggregateDecryptionKey, AggregatePublicKey, CipherText, DecryptionKeyShare, PublicKeyShare,
10    SecretKeyShare, create_dk_share, decrypt_preimage, encrypt_preimage, verify_agg_dk,
11    verify_ciphertext, verify_dk_share,
12};
13
14use crate::ContractId;
15
16#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
17pub enum PaymentImage {
18    Hash(sha256::Hash),
19    Point(PublicKey),
20}
21
22#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
23pub struct IncomingContract {
24    pub commitment: Commitment,
25    pub ciphertext: CipherText,
26}
27
28#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
29pub struct Commitment {
30    pub payment_image: PaymentImage,
31    pub amount: Amount,
32    /// A real unix-time expiration for ordinary incoming contracts, or a
33    /// fee-encoded value for lnurl receives (see [`fee_encoded_expiration`]).
34    /// The wire name stays `expiration` for backwards compatibility.
35    #[serde(rename = "expiration")]
36    pub expiration_or_fee: u64,
37    pub claim_pk: PublicKey,
38    pub refund_pk: PublicKey,
39    pub ephemeral_pk: PublicKey,
40}
41
42/// Encode a gateway fee into an incoming-contract expiration.
43///
44/// Incoming contracts created for lnurl receives store the gateway fee here
45/// instead of a real timestamp: the receiving client never sees the invoice for
46/// these contracts, so this is the only way for it to recover the fee and
47/// report the invoice amount in its payment events. A real expiration is not
48/// needed as these contracts are discovered via the contract stream rather than
49/// awaited per-invoice, and the funding-time validation only requires the
50/// expiration to lie in the future, which the resulting near-`u64::MAX` value
51/// satisfies.
52pub fn fee_encoded_expiration(fee_msats: u64) -> u64 {
53    u64::MAX - fee_msats
54}
55
56/// Recover the gateway fee from an incoming-contract expiration created with
57/// [`fee_encoded_expiration`].
58pub fn fee_from_expiration(expiration: u64) -> u64 {
59    u64::MAX - expiration
60}
61
62impl IncomingContract {
63    #[allow(clippy::too_many_arguments)]
64    pub fn new(
65        agg_pk: AggregatePublicKey,
66        encryption_seed: [u8; 32],
67        preimage: [u8; 32],
68        payment_image: PaymentImage,
69        amount: Amount,
70        expiration: u64,
71        claim_pk: PublicKey,
72        refund_pk: PublicKey,
73        ephemeral_pk: PublicKey,
74    ) -> Self {
75        let commitment = Commitment {
76            payment_image,
77            amount,
78            expiration_or_fee: expiration,
79            claim_pk,
80            refund_pk,
81            ephemeral_pk,
82        };
83
84        let ciphertext = encrypt_preimage(
85            &agg_pk,
86            &encryption_seed,
87            &preimage,
88            &commitment.consensus_hash(),
89        );
90
91        IncomingContract {
92            commitment,
93            ciphertext,
94        }
95    }
96
97    pub fn contract_id(&self) -> ContractId {
98        ContractId(self.consensus_hash())
99    }
100
101    pub fn verify(&self) -> bool {
102        verify_ciphertext(&self.ciphertext, &self.commitment.consensus_hash())
103    }
104
105    pub fn verify_decryption_share(
106        &self,
107        pk: &PublicKeyShare,
108        dk_share: &DecryptionKeyShare,
109    ) -> bool {
110        verify_dk_share(
111            pk,
112            dk_share,
113            &self.ciphertext,
114            &self.commitment.consensus_hash(),
115        )
116    }
117
118    pub fn verify_agg_decryption_key(
119        &self,
120        agg_pk: &AggregatePublicKey,
121        agg_decryption_key: &AggregateDecryptionKey,
122    ) -> bool {
123        verify_agg_dk(
124            agg_pk,
125            agg_decryption_key,
126            &self.ciphertext,
127            &self.commitment.consensus_hash(),
128        )
129    }
130
131    pub fn verify_preimage(&self, preimage: &[u8; 32]) -> bool {
132        verify_preimage(&self.commitment.payment_image, preimage)
133    }
134
135    pub fn decrypt_preimage(
136        &self,
137        agg_decryption_key: &AggregateDecryptionKey,
138    ) -> Option<[u8; 32]> {
139        let preimage = decrypt_preimage(&self.ciphertext, agg_decryption_key);
140
141        if self.verify_preimage(&preimage) {
142            Some(preimage)
143        } else {
144            None
145        }
146    }
147
148    pub fn create_decryption_key_share(&self, sk: &SecretKeyShare) -> DecryptionKeyShare {
149        create_dk_share(sk, &self.ciphertext)
150    }
151}
152
153#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, Encodable, Decodable)]
154pub struct OutgoingContract {
155    pub payment_image: PaymentImage,
156    pub amount: Amount,
157    pub expiration: u64,
158    pub claim_pk: PublicKey,
159    pub refund_pk: PublicKey,
160    pub ephemeral_pk: PublicKey,
161}
162
163impl OutgoingContract {
164    pub fn contract_id(&self) -> ContractId {
165        ContractId(self.consensus_hash())
166    }
167
168    pub fn forfeit_message(&self) -> Message {
169        Message::from_digest(*self.contract_id().0.as_ref())
170    }
171
172    pub fn verify_preimage(&self, preimage: &[u8; 32]) -> bool {
173        verify_preimage(&self.payment_image, preimage)
174    }
175
176    pub fn verify_forfeit_signature(&self, signature: &Signature) -> bool {
177        secp256k1::global::SECP256K1
178            .verify_schnorr(
179                signature,
180                &self.forfeit_message(),
181                &self.claim_pk.x_only_public_key().0,
182            )
183            .is_ok()
184    }
185
186    pub fn verify_gateway_response(&self, gateway_response: &Result<[u8; 32], Signature>) -> bool {
187        match gateway_response {
188            Ok(preimage) => self.verify_preimage(preimage),
189            Err(signature) => self.verify_forfeit_signature(signature),
190        }
191    }
192
193    pub fn verify_invoice_auth(&self, message: sha256::Hash, signature: &Signature) -> bool {
194        secp256k1::global::SECP256K1
195            .verify_schnorr(
196                signature,
197                &Message::from_digest(*message.as_ref()),
198                &self.refund_pk.x_only_public_key().0,
199            )
200            .is_ok()
201    }
202}
203
204fn verify_preimage(payment_image: &PaymentImage, preimage: &[u8; 32]) -> bool {
205    match payment_image {
206        PaymentImage::Hash(hash) => preimage.consensus_hash::<sha256::Hash>() == *hash,
207        PaymentImage::Point(pk) => match SecretKey::from_slice(preimage) {
208            Ok(sk) => sk.public_key(secp256k1::SECP256K1) == *pk,
209            Err(..) => false,
210        },
211    }
212}
213
214#[test]
215fn test_verify_preimage() {
216    use bitcoin::hashes::Hash;
217
218    assert!(verify_preimage(
219        &PaymentImage::Hash(bitcoin::hashes::sha256::Hash::hash(&[42; 32])),
220        &[42; 32]
221    ));
222
223    let (secret_key, public_key) = secp256k1::generate_keypair(&mut secp256k1::rand::thread_rng());
224
225    assert!(verify_preimage(
226        &PaymentImage::Point(public_key),
227        &secret_key.secret_bytes()
228    ));
229}