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 Ok(Self(r.read_byte_array()?))
142 }
143}
144
145#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
153pub struct BlindedMailboxIdentifier([u8; PUBLIC_KEY_SIZE]);
154
155impl_byte_newtype!(BlindedMailboxIdentifier, PUBLIC_KEY_SIZE);
156
157impl BlindedMailboxIdentifier {
158 pub fn new(
159 mailbox_id: MailboxIdentifier,
160 server_pubkey: PublicKey,
161 vtxo_key: &Keypair,
162 ) -> BlindedMailboxIdentifier {
163 let dh = ecdh::shared_secret_point(&server_pubkey, &vtxo_key.secret_key());
164 let dh_pk = point_to_pubkey(&dh);
165 let ret = PublicKey::combine_keys(&[&mailbox_id.as_pubkey(), &dh_pk])
166 .expect("error adding DH secret to mailbox key");
167 Self(ret.serialize())
168 }
169
170 pub fn as_pubkey(&self) -> PublicKey {
172 PublicKey::from_slice(&self.0).expect("invalid pubkey")
173 }
174
175 pub fn from_pubkey(pubkey: PublicKey) -> Self {
177 Self(pubkey.serialize())
178 }
179}
180
181impl From<PublicKey> for BlindedMailboxIdentifier {
182 fn from(pk: PublicKey) -> Self {
183 Self::from_pubkey(pk)
184 }
185}
186
187impl ProtocolEncoding for BlindedMailboxIdentifier {
188 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
189 w.emit_slice(self.as_ref())
190 }
191
192 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
193 Ok(Self(r.read_byte_array()?))
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
201pub struct MailboxAuthorization {
202 id: MailboxIdentifier,
203 expiry: i64,
204 sig: schnorr::Signature,
205}
206
207impl MailboxAuthorization {
208 const CHALENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO mailbox authorization: ";
209
210 fn signable_message(expiry: i64) -> Message {
211 let mut eng = sha256::Hash::engine();
212 eng.input(Self::CHALENGE_MESSAGE_PREFIX);
213 eng.input(&expiry.to_le_bytes());
214 Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
215 }
216
217 pub fn new(
218 mailbox_key: &Keypair,
219 expiry: chrono::DateTime<chrono::Local>,
220 ) -> MailboxAuthorization {
221 let expiry = expiry.timestamp();
222 let msg = Self::signable_message(expiry);
223 MailboxAuthorization {
224 id: MailboxIdentifier::from_pubkey(mailbox_key.public_key()),
225 expiry: expiry,
226 sig: SECP.sign_schnorr_with_aux_rand(&msg, mailbox_key, &rand::random()),
227 }
228 }
229
230 pub fn mailbox(&self) -> MailboxIdentifier {
232 self.id
233 }
234
235 pub fn expiry(&self) -> chrono::DateTime<chrono::Local> {
237 chrono::DateTime::from_timestamp_secs(self.expiry)
238 .expect("we guarantee valid timestamp")
239 .with_timezone(&chrono::Local)
240 }
241
242 pub fn verify(&self) -> bool {
244 let msg = Self::signable_message(self.expiry);
245 SECP.verify_schnorr(&self.sig, &msg, &self.id.as_pubkey().into()).is_ok()
246 }
247
248 pub fn is_expired(&self) -> bool {
249 self.expiry() < chrono::Local::now()
250 }
251}
252
253impl ProtocolEncoding for MailboxAuthorization {
254 fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
255 self.id.encode(w)?;
256 w.emit_slice(&self.expiry.to_le_bytes())?;
257 self.sig.encode(w)?;
258 Ok(())
259 }
260
261 fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
262 Ok(Self {
263 id: ProtocolEncoding::decode(r)?,
264 expiry: {
265 let timestamp = i64::from_le_bytes(r.read_byte_array()?);
266 let _ = chrono::DateTime::from_timestamp_secs(timestamp)
268 .ok_or_else(|| ProtocolDecodingError::invalid("invalid timestamp"))?;
269 timestamp
270 },
271 sig: ProtocolEncoding::decode(r)?,
272 })
273 }
274}
275
276fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
278 let mut uncompressed = [0u8; 65];
280 uncompressed[0] = 0x04;
281 uncompressed[1..].copy_from_slice(point);
282 PublicKey::from_slice(&uncompressed).expect("invalid uncompressed pk")
283}
284
285#[cfg(test)]
286mod test {
287 use std::time::Duration;
288 use bitcoin::secp256k1::rand;
289 use super::*;
290
291 #[test]
292 fn mailbox_blinding() {
293 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
294 let server_mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
295 let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
296
297 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
298
299 let blinded = mailbox.to_blinded(server_mailbox_key.public_key(), &vtxo_key);
300
301 let unblinded = MailboxIdentifier::from_blinded(
302 blinded, vtxo_key.public_key(), &server_mailbox_key,
303 );
304
305 assert_eq!(unblinded, mailbox);
306 }
307
308 #[test]
309 fn mailbox_authorization() {
310 let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
311 let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
312
313 let expiry = chrono::Local::now() + Duration::from_secs(60);
314 let auth = MailboxAuthorization::new(&mailbox_key, expiry);
315 assert_eq!(auth.mailbox(), mailbox);
316 assert!(auth.verify());
317
318 assert_eq!(auth, MailboxAuthorization::deserialize(&auth.serialize()).unwrap());
319
320 let decoded = MailboxAuthorization::deserialize_hex("023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbeac3380369000000000163b3184156804eb26ffbad964a70840229c4ac80da5da9f9a7557874c45259af48671aa26f567c3c855092c51a1ceeb8a17c7540abe0a50e89866bdb90ece9").unwrap();
322 assert_eq!(decoded.expiry, 1761818819);
323 assert_eq!(decoded.id.to_string(), "023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbea");
324 assert!(decoded.verify());
325 }
326
327 #[test]
328 fn mailbox_type_round_trip() {
329 let ar = MailboxType::ArkoorReceive;
330 let rpc = MailboxType::RoundParticipationCompleted;
331 let ln = MailboxType::LnRecvPendingPayment;
332 let rvi = MailboxType::RecoveryVtxoId;
333
334 let cases = [
335 (ar, u32::from(ar), ar.as_str()),
336 (rpc, u32::from(rpc), rpc.as_str()),
337 (ln, u32::from(ln), ln.as_str()),
338 (rvi, u32::from(rvi), rvi.as_str()),
339 ];
340
341 let mut seen_u32 = std::collections::HashSet::new();
342
343 for (variant, expected_u32, expected_str) in cases {
344 let actual = u32::from(variant);
345 assert_eq!(actual, expected_u32, "wrong u32 for {:?}", variant);
346
347 let actual = String::from(variant);
348 assert_eq!(actual, expected_str, "wrong str for {:?}", variant);
349
350 let round_trip = actual.parse::<MailboxType>().unwrap();
351 assert_eq!(round_trip, variant);
352
353 assert!(seen_u32.insert(expected_u32), "duplicate u32 value: {}", expected_u32);
354 }
355
356 assert!(MailboxType::try_from(cases.len() as u32).is_err());
357 assert!(MailboxType::try_from(u32::MAX).is_err());
358 assert!(MailboxType::try_from(999_999).is_err());
359 assert!(MailboxType::from_str("arkor_receive").is_err()); assert!(MailboxType::from_str("").is_err());
361 assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); }
363}
364