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 ) -> Result<BlindedMailboxIdentifier, MailboxBlindingError> {
116 BlindedMailboxIdentifier::new(*self, server_pubkey, vtxo_key)
117 }
118
119 pub fn from_blinded(
125 blinded: BlindedMailboxIdentifier,
126 vtxo_pubkey: PublicKey,
127 server_key: &Keypair,
128 ) -> Result<MailboxIdentifier, MailboxBlindingError> {
129 let dh = ecdh::shared_secret_point(&vtxo_pubkey, &server_key.secret_key());
130 let neg_dh_pk = point_to_pubkey(&dh).negate(&SECP);
131 let ret = PublicKey::combine_keys(&[&blinded.as_pubkey(), &neg_dh_pk])
132 .map_err(|_| MailboxBlindingError)?;
133 Ok(Self(ret))
134 }
135}
136
137#[derive(Debug, thiserror::Error)]
142#[error("mailbox blinding produced the point at infinity")]
143pub struct MailboxBlindingError;
144
145impl fmt::Display for MailboxIdentifier {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 write!(f, "{}", self.0)
148 }
149}
150
151#[derive(Debug, thiserror::Error)]
152#[error("invalid mailbox identifier: {0}")]
153pub struct InvalidMailboxIdentifier(String);
154
155impl FromStr for MailboxIdentifier {
156 type Err = InvalidMailboxIdentifier;
157
158 fn from_str(s: &str) -> Result<Self, Self::Err> {
159 let pubkey = PublicKey::from_str(s).map_err(|_| InvalidMailboxIdentifier(s.to_string()))?;
160 Ok(Self(pubkey))
161 }
162}
163
164impl From<PublicKey> for MailboxIdentifier {
165 fn from(pk: PublicKey) -> Self {
166 Self::from_pubkey(pk)
167 }
168}
169
170impl ProtocolEncoding for MailboxIdentifier {
171 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
172 w.emit_slice(self.0.serialize().as_slice())
173 }
174
175 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
176 let bytes: [u8; PUBLIC_KEY_SIZE] = r.read_byte_array()?;
177 let pubkey = PublicKey::from_slice(&bytes).map_err(|e| {
178 ProtocolDecodingError::invalid_err(e, "invalid mailbox identifier public key")
179 })?;
180 Ok(Self(pubkey))
181 }
182}
183
184#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
192pub struct BlindedMailboxIdentifier([u8; PUBLIC_KEY_SIZE]);
193
194impl_byte_newtype!(BlindedMailboxIdentifier, PUBLIC_KEY_SIZE);
195
196impl BlindedMailboxIdentifier {
197 pub fn new(
198 mailbox_id: MailboxIdentifier,
199 server_pubkey: PublicKey,
200 vtxo_key: &Keypair,
201 ) -> Result<BlindedMailboxIdentifier, MailboxBlindingError> {
202 let dh = ecdh::shared_secret_point(&server_pubkey, &vtxo_key.secret_key());
203 let dh_pk = point_to_pubkey(&dh);
204 let ret = PublicKey::combine_keys(&[&mailbox_id.as_pubkey(), &dh_pk])
205 .map_err(|_| MailboxBlindingError)?;
206 Ok(Self(ret.serialize()))
207 }
208
209 pub fn as_pubkey(&self) -> PublicKey {
211 PublicKey::from_slice(&self.0).expect("invalid pubkey")
212 }
213
214 pub fn from_pubkey(pubkey: PublicKey) -> Self {
216 Self(pubkey.serialize())
217 }
218}
219
220impl From<PublicKey> for BlindedMailboxIdentifier {
221 fn from(pk: PublicKey) -> Self {
222 Self::from_pubkey(pk)
223 }
224}
225
226impl ProtocolEncoding for BlindedMailboxIdentifier {
227 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
228 w.emit_slice(self.as_ref())
229 }
230
231 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
232 let bytes: [u8; PUBLIC_KEY_SIZE] = r.read_byte_array()?;
233 PublicKey::from_slice(&bytes).map_err(|e| {
234 ProtocolDecodingError::invalid_err(e, "invalid blinded mailbox identifier public key")
235 })?;
236 Ok(Self(bytes))
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
244pub struct MailboxAuthorization {
245 id: MailboxIdentifier,
246 expiry: i64,
247 sig: schnorr::Signature,
248}
249
250impl MailboxAuthorization {
251 const CHALENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO mailbox authorization: ";
252
253 fn signable_message(expiry: i64) -> Message {
254 let mut eng = sha256::Hash::engine();
255 eng.input(Self::CHALENGE_MESSAGE_PREFIX);
256 eng.input(&expiry.to_le_bytes());
257 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
258 }
259
260 pub fn new(
261 mailbox_key: &Keypair,
262 expiry: chrono::DateTime<chrono::Local>,
263 ) -> MailboxAuthorization {
264 let expiry = expiry.timestamp();
265 let msg = Self::signable_message(expiry);
266 MailboxAuthorization {
267 id: MailboxIdentifier::from_pubkey(mailbox_key.public_key()),
268 expiry: expiry,
269 sig: SECP.sign_schnorr_with_aux_rand(&msg, mailbox_key, &rand::random()),
270 }
271 }
272
273 pub fn mailbox(&self) -> MailboxIdentifier {
275 self.id
276 }
277
278 pub fn expiry(&self) -> chrono::DateTime<chrono::Local> {
280 chrono::DateTime::from_timestamp_secs(self.expiry)
281 .expect("we guarantee valid timestamp")
282 .with_timezone(&chrono::Local)
283 }
284
285 pub fn verify(&self) -> bool {
287 let msg = Self::signable_message(self.expiry);
288 SECP.verify_schnorr(&self.sig, &msg, &self.id.as_pubkey().into()).is_ok()
289 }
290
291 pub fn is_expired(&self) -> bool {
295 const LEEWAY: Duration = Duration::from_secs(5);
297 self.expiry() < (chrono::Local::now() - LEEWAY)
298 }
299}
300
301impl ProtocolEncoding for MailboxAuthorization {
302 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
303 self.id.encode(w)?;
304 w.emit_slice(&self.expiry.to_le_bytes())?;
305 self.sig.encode(w)?;
306 Ok(())
307 }
308
309 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
310 Ok(Self {
311 id: ProtocolEncoding::decode(r)?,
312 expiry: {
313 let timestamp = i64::from_le_bytes(r.read_byte_array()?);
314 let _ = chrono::DateTime::from_timestamp_secs(timestamp)
316 .ok_or_else(|| ProtocolDecodingError::invalid("invalid timestamp"))?;
317 timestamp
318 },
319 sig: ProtocolEncoding::decode(r)?,
320 })
321 }
322}
323
324fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
326 let mut uncompressed = [0u8; 65];
328 uncompressed[0] = 0x04;
329 uncompressed[1..].copy_from_slice(point);
330 PublicKey::from_slice(&uncompressed).expect("invalid uncompressed pk")
331}
332
333#[cfg(test)]
334mod test {
335 use std::time::Duration;
336 use bitcoin::secp256k1::rand;
337 use super::*;
338
339 #[test]
340 fn mailbox_blinding() {
341 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
342 let server_mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
343 let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
344
345 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
346
347 let blinded = mailbox.to_blinded(server_mailbox_key.public_key(), &vtxo_key)
348 .expect("blinding a random mailbox id should succeed");
349
350 let unblinded = MailboxIdentifier::from_blinded(
351 blinded, vtxo_key.public_key(), &server_mailbox_key,
352 ).expect("unblinding a valid blinded id should succeed");
353
354 assert_eq!(unblinded, mailbox);
355 }
356
357 #[test]
363 fn from_blinded_rejects_point_at_infinity() {
364 let server_mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
365 let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
366
367 let dh = ecdh::shared_secret_point(
371 &server_mailbox_key.public_key(), &vtxo_key.secret_key(),
372 );
373 let blinded = BlindedMailboxIdentifier::from_pubkey(point_to_pubkey(&dh));
374
375 let res = MailboxIdentifier::from_blinded(
376 blinded, vtxo_key.public_key(), &server_mailbox_key,
377 );
378 assert!(res.is_err(), "expected identity-point error, got {:?}", res);
379 }
380
381 #[test]
386 fn to_blinded_rejects_point_at_infinity() {
387 let server_mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
388 let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
389
390 let dh = ecdh::shared_secret_point(
391 &server_mailbox_key.public_key(), &vtxo_key.secret_key(),
392 );
393 let neg_dh = point_to_pubkey(&dh).negate(&SECP);
394 let mailbox = MailboxIdentifier::from_pubkey(neg_dh);
395
396 let res = mailbox.to_blinded(server_mailbox_key.public_key(), &vtxo_key);
397 assert!(res.is_err(), "expected identity-point error, got {:?}", res);
398 }
399
400 #[test]
401 fn mailbox_authorization() {
402 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
403 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
404
405 let expiry = chrono::Local::now() + Duration::from_secs(60);
406 let auth = MailboxAuthorization::new(&mailbox_key, expiry);
407 assert_eq!(auth.mailbox(), mailbox);
408 assert!(auth.verify());
409
410 assert_eq!(auth, MailboxAuthorization::deserialize(&auth.serialize()).unwrap());
411
412 let decoded = MailboxAuthorization::deserialize_hex("023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbeac3380369000000000163b3184156804eb26ffbad964a70840229c4ac80da5da9f9a7557874c45259af48671aa26f567c3c855092c51a1ceeb8a17c7540abe0a50e89866bdb90ece9").unwrap();
414 assert_eq!(decoded.expiry, 1761818819);
415 assert_eq!(decoded.id.to_string(), "023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbea");
416 assert!(decoded.verify());
417 }
418
419 #[test]
420 fn mailbox_type_round_trip() {
421 let ar = MailboxType::ArkoorReceive;
422 let rpc = MailboxType::RoundParticipationCompleted;
423 let ln = MailboxType::LnRecvPendingPayment;
424 let rvi = MailboxType::RecoveryVtxoId;
425 let lsf = MailboxType::LnSendFinished;
426
427 let cases = [
428 (ar, u32::from(ar), ar.as_str()),
429 (rpc, u32::from(rpc), rpc.as_str()),
430 (ln, u32::from(ln), ln.as_str()),
431 (rvi, u32::from(rvi), rvi.as_str()),
432 (lsf, u32::from(lsf), lsf.as_str()),
433 ];
434
435 let mut seen_u32 = std::collections::HashSet::new();
436
437 for (variant, expected_u32, expected_str) in cases {
438 let actual = u32::from(variant);
439 assert_eq!(actual, expected_u32, "wrong u32 for {:?}", variant);
440
441 let actual = String::from(variant);
442 assert_eq!(actual, expected_str, "wrong str for {:?}", variant);
443
444 let round_trip = actual.parse::<MailboxType>().unwrap();
445 assert_eq!(round_trip, variant);
446
447 assert!(seen_u32.insert(expected_u32), "duplicate u32 value: {}", expected_u32);
448 }
449
450 assert!(MailboxType::try_from(cases.len() as u32).is_err());
451 assert!(MailboxType::try_from(u32::MAX).is_err());
452 assert!(MailboxType::try_from(999_999).is_err());
453 assert!(MailboxType::from_str("arkor_receive").is_err()); assert!(MailboxType::from_str("").is_err());
455 assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); }
457}
458