1use std::time::Duration;
6use std::{fmt, io};
7use std::str::FromStr;
8use bitcoin::hashes::{sha256, Hash, HashEngine};
9use bitcoin::secp256k1::{ecdh, schnorr, Keypair, Message, PublicKey};
10use bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE;
11
12use crate::SECP;
13use crate::encode::{ProtocolDecodingError, ProtocolEncoding, ReadExt, WriteExt};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum MailboxType {
17 ArkoorReceive,
18 RoundParticipationCompleted,
19 LnRecvPendingPayment,
20 RecoveryVtxoId,
21 LnSendFinished,
22}
23
24impl MailboxType {
25 #[inline]
26 pub const fn as_str(self) -> &'static str {
27 match self {
28 MailboxType::ArkoorReceive => "arkoor-receive",
29 MailboxType::RoundParticipationCompleted => "round-participation-completed",
30 MailboxType::LnRecvPendingPayment => "ln-recv-pending",
31 MailboxType::RecoveryVtxoId => "recovery-vtxo-id",
32 MailboxType::LnSendFinished => "ln-send-finished",
33 }
34 }
35
36}
37
38impl fmt::Display for MailboxType {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 f.write_str(self.as_str())
41 }
42}
43
44impl TryFrom<u32> for MailboxType {
45 type Error = &'static str;
46
47 fn try_from(i: u32) -> Result<Self, Self::Error> {
48 match i {
49 0 => Ok(MailboxType::ArkoorReceive),
50 1 => Ok(MailboxType::RoundParticipationCompleted),
51 2 => Ok(MailboxType::LnRecvPendingPayment),
52 3 => Ok(MailboxType::RecoveryVtxoId),
53 4 => Ok(MailboxType::LnSendFinished),
54 _ => Err("invalid mailbox type"),
55 }
56 }
57}
58
59impl From<MailboxType> for u32 {
60 fn from(t: MailboxType) -> Self {
61 match t {
62 MailboxType::ArkoorReceive => 0,
63 MailboxType::RoundParticipationCompleted => 1,
64 MailboxType::LnRecvPendingPayment => 2,
65 MailboxType::RecoveryVtxoId => 3,
66 MailboxType::LnSendFinished => 4,
67 }
68 }
69}
70
71impl From<MailboxType> for String {
72 fn from(t: MailboxType) -> Self {
73 t.as_str().to_string()
74 }
75}
76
77impl FromStr for MailboxType {
78 type Err = &'static str;
79
80 fn from_str(s: &str) -> Result<Self, Self::Err> {
81 match s {
82 v if v == MailboxType::ArkoorReceive.as_str() => Ok(MailboxType::ArkoorReceive),
83 v if v == MailboxType::RoundParticipationCompleted.as_str() => Ok(MailboxType::RoundParticipationCompleted),
84 v if v == MailboxType::LnRecvPendingPayment.as_str() => Ok(MailboxType::LnRecvPendingPayment),
85 v if v == MailboxType::RecoveryVtxoId.as_str() => Ok(MailboxType::RecoveryVtxoId),
86 v if v == MailboxType::LnSendFinished.as_str() => Ok(MailboxType::LnSendFinished),
87 _ => Err("invalid mailbox type"),
88 }
89 }
90}
91
92
93#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
97pub struct MailboxIdentifier(PublicKey);
98
99impl MailboxIdentifier {
100 pub fn as_pubkey(&self) -> PublicKey {
102 self.0
103 }
104
105 pub fn from_pubkey(pubkey: PublicKey) -> Self {
107 Self(pubkey)
108 }
109
110 pub fn to_blinded(
112 &self,
113 server_pubkey: PublicKey,
114 vtxo_key: &Keypair,
115 ) -> BlindedMailboxIdentifier {
116 BlindedMailboxIdentifier::new(*self, server_pubkey, vtxo_key)
117 }
118
119 pub fn from_blinded(
121 blinded: BlindedMailboxIdentifier,
122 vtxo_pubkey: PublicKey,
123 server_key: &Keypair,
124 ) -> MailboxIdentifier {
125 let dh = ecdh::shared_secret_point(&vtxo_pubkey, &server_key.secret_key());
126 let neg_dh_pk = point_to_pubkey(&dh).negate(&SECP);
127 let ret = PublicKey::combine_keys(&[&blinded.as_pubkey(), &neg_dh_pk])
128 .expect("error adding DH secret to mailbox key");
129 Self(ret)
130 }
131}
132
133impl fmt::Display for MailboxIdentifier {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{}", self.0)
136 }
137}
138
139#[derive(Debug, thiserror::Error)]
140#[error("invalid mailbox identifier: {0}")]
141pub struct InvalidMailboxIdentifier(String);
142
143impl FromStr for MailboxIdentifier {
144 type Err = InvalidMailboxIdentifier;
145
146 fn from_str(s: &str) -> Result<Self, Self::Err> {
147 let pubkey = PublicKey::from_str(s).map_err(|_| InvalidMailboxIdentifier(s.to_string()))?;
148 Ok(Self(pubkey))
149 }
150}
151
152impl From<PublicKey> for MailboxIdentifier {
153 fn from(pk: PublicKey) -> Self {
154 Self::from_pubkey(pk)
155 }
156}
157
158impl ProtocolEncoding for MailboxIdentifier {
159 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
160 w.emit_slice(self.0.serialize().as_slice())
161 }
162
163 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
164 let bytes: [u8; PUBLIC_KEY_SIZE] = r.read_byte_array()?;
165 let pubkey = PublicKey::from_slice(&bytes).map_err(|e| {
166 ProtocolDecodingError::invalid_err(e, "invalid mailbox identifier public key")
167 })?;
168 Ok(Self(pubkey))
169 }
170}
171
172#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
180pub struct BlindedMailboxIdentifier([u8; PUBLIC_KEY_SIZE]);
181
182impl_byte_newtype!(BlindedMailboxIdentifier, PUBLIC_KEY_SIZE);
183
184impl BlindedMailboxIdentifier {
185 pub fn new(
186 mailbox_id: MailboxIdentifier,
187 server_pubkey: PublicKey,
188 vtxo_key: &Keypair,
189 ) -> BlindedMailboxIdentifier {
190 let dh = ecdh::shared_secret_point(&server_pubkey, &vtxo_key.secret_key());
191 let dh_pk = point_to_pubkey(&dh);
192 let ret = PublicKey::combine_keys(&[&mailbox_id.as_pubkey(), &dh_pk])
193 .expect("error adding DH secret to mailbox key");
194 Self(ret.serialize())
195 }
196
197 pub fn as_pubkey(&self) -> PublicKey {
199 PublicKey::from_slice(&self.0).expect("invalid pubkey")
200 }
201
202 pub fn from_pubkey(pubkey: PublicKey) -> Self {
204 Self(pubkey.serialize())
205 }
206}
207
208impl From<PublicKey> for BlindedMailboxIdentifier {
209 fn from(pk: PublicKey) -> Self {
210 Self::from_pubkey(pk)
211 }
212}
213
214impl ProtocolEncoding for BlindedMailboxIdentifier {
215 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
216 w.emit_slice(self.as_ref())
217 }
218
219 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
220 let bytes: [u8; PUBLIC_KEY_SIZE] = r.read_byte_array()?;
221 PublicKey::from_slice(&bytes).map_err(|e| {
222 ProtocolDecodingError::invalid_err(e, "invalid blinded mailbox identifier public key")
223 })?;
224 Ok(Self(bytes))
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
232pub struct MailboxAuthorization {
233 id: MailboxIdentifier,
234 expiry: i64,
235 sig: schnorr::Signature,
236}
237
238impl MailboxAuthorization {
239 const CHALENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO mailbox authorization: ";
240
241 fn signable_message(expiry: i64) -> Message {
242 let mut eng = sha256::Hash::engine();
243 eng.input(Self::CHALENGE_MESSAGE_PREFIX);
244 eng.input(&expiry.to_le_bytes());
245 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
246 }
247
248 pub fn new(
249 mailbox_key: &Keypair,
250 expiry: chrono::DateTime<chrono::Local>,
251 ) -> MailboxAuthorization {
252 let expiry = expiry.timestamp();
253 let msg = Self::signable_message(expiry);
254 MailboxAuthorization {
255 id: MailboxIdentifier::from_pubkey(mailbox_key.public_key()),
256 expiry: expiry,
257 sig: SECP.sign_schnorr_with_aux_rand(&msg, mailbox_key, &rand::random()),
258 }
259 }
260
261 pub fn mailbox(&self) -> MailboxIdentifier {
263 self.id
264 }
265
266 pub fn expiry(&self) -> chrono::DateTime<chrono::Local> {
268 chrono::DateTime::from_timestamp_secs(self.expiry)
269 .expect("we guarantee valid timestamp")
270 .with_timezone(&chrono::Local)
271 }
272
273 pub fn verify(&self) -> bool {
275 let msg = Self::signable_message(self.expiry);
276 SECP.verify_schnorr(&self.sig, &msg, &self.id.as_pubkey().into()).is_ok()
277 }
278
279 pub fn is_expired(&self) -> bool {
283 const LEEWAY: Duration = Duration::from_secs(5);
285 self.expiry() < (chrono::Local::now() - LEEWAY)
286 }
287}
288
289impl ProtocolEncoding for MailboxAuthorization {
290 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
291 self.id.encode(w)?;
292 w.emit_slice(&self.expiry.to_le_bytes())?;
293 self.sig.encode(w)?;
294 Ok(())
295 }
296
297 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
298 Ok(Self {
299 id: ProtocolEncoding::decode(r)?,
300 expiry: {
301 let timestamp = i64::from_le_bytes(r.read_byte_array()?);
302 let _ = chrono::DateTime::from_timestamp_secs(timestamp)
304 .ok_or_else(|| ProtocolDecodingError::invalid("invalid timestamp"))?;
305 timestamp
306 },
307 sig: ProtocolEncoding::decode(r)?,
308 })
309 }
310}
311
312fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
314 let mut uncompressed = [0u8; 65];
316 uncompressed[0] = 0x04;
317 uncompressed[1..].copy_from_slice(point);
318 PublicKey::from_slice(&uncompressed).expect("invalid uncompressed pk")
319}
320
321#[cfg(test)]
322mod test {
323 use std::time::Duration;
324 use bitcoin::secp256k1::rand;
325 use super::*;
326
327 #[test]
328 fn mailbox_blinding() {
329 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
330 let server_mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
331 let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
332
333 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
334
335 let blinded = mailbox.to_blinded(server_mailbox_key.public_key(), &vtxo_key);
336
337 let unblinded = MailboxIdentifier::from_blinded(
338 blinded, vtxo_key.public_key(), &server_mailbox_key,
339 );
340
341 assert_eq!(unblinded, mailbox);
342 }
343
344 #[test]
345 fn mailbox_authorization() {
346 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
347 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
348
349 let expiry = chrono::Local::now() + Duration::from_secs(60);
350 let auth = MailboxAuthorization::new(&mailbox_key, expiry);
351 assert_eq!(auth.mailbox(), mailbox);
352 assert!(auth.verify());
353
354 assert_eq!(auth, MailboxAuthorization::deserialize(&auth.serialize()).unwrap());
355
356 let decoded = MailboxAuthorization::deserialize_hex("023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbeac3380369000000000163b3184156804eb26ffbad964a70840229c4ac80da5da9f9a7557874c45259af48671aa26f567c3c855092c51a1ceeb8a17c7540abe0a50e89866bdb90ece9").unwrap();
358 assert_eq!(decoded.expiry, 1761818819);
359 assert_eq!(decoded.id.to_string(), "023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbea");
360 assert!(decoded.verify());
361 }
362
363 #[test]
364 fn mailbox_type_round_trip() {
365 let ar = MailboxType::ArkoorReceive;
366 let rpc = MailboxType::RoundParticipationCompleted;
367 let ln = MailboxType::LnRecvPendingPayment;
368 let rvi = MailboxType::RecoveryVtxoId;
369 let lsf = MailboxType::LnSendFinished;
370
371 let cases = [
372 (ar, u32::from(ar), ar.as_str()),
373 (rpc, u32::from(rpc), rpc.as_str()),
374 (ln, u32::from(ln), ln.as_str()),
375 (rvi, u32::from(rvi), rvi.as_str()),
376 (lsf, u32::from(lsf), lsf.as_str()),
377 ];
378
379 let mut seen_u32 = std::collections::HashSet::new();
380
381 for (variant, expected_u32, expected_str) in cases {
382 let actual = u32::from(variant);
383 assert_eq!(actual, expected_u32, "wrong u32 for {:?}", variant);
384
385 let actual = String::from(variant);
386 assert_eq!(actual, expected_str, "wrong str for {:?}", variant);
387
388 let round_trip = actual.parse::<MailboxType>().unwrap();
389 assert_eq!(round_trip, variant);
390
391 assert!(seen_u32.insert(expected_u32), "duplicate u32 value: {}", expected_u32);
392 }
393
394 assert!(MailboxType::try_from(cases.len() as u32).is_err());
395 assert!(MailboxType::try_from(u32::MAX).is_err());
396 assert!(MailboxType::try_from(999_999).is_err());
397 assert!(MailboxType::from_str("arkor_receive").is_err()); assert!(MailboxType::from_str("").is_err());
399 assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); }
401}
402