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