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