Skip to main content

ark/
mailbox.rs

1//! Types for using the Unified Mailbox feature of the bark server.
2//!
3//! For more information on the mailbox, check the `docs/mailbox.md` file.
4
5use 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/// Identifier for a mailbox
88///
89/// Represented as a curve point.
90#[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	/// Convert to public key
97	pub fn as_pubkey(&self) -> PublicKey {
98		PublicKey::from_slice(&self.0).expect("invalid pubkey")
99	}
100
101	/// Convert from a public key
102	pub fn from_pubkey(pubkey: PublicKey) -> Self {
103		Self(pubkey.serialize())
104	}
105
106	/// Blind the mailbox id with the server pubkey and the VTXO privkey
107	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	/// Unblind a blinded mailbox identifier
116	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/// Blinded identifier for a mailbox
150///
151/// It is blinded by adding to the mailbox public key point the
152/// Diffie-Hellman secret between the server's key and the VTXO key from
153/// the address.
154///
155/// Represented as a curve point.
156#[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	/// Convert to public key
175	pub fn as_pubkey(&self) -> PublicKey {
176		PublicKey::from_slice(&self.0).expect("invalid pubkey")
177	}
178
179	/// Convert from a public key
180	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/// Authorization to read a VTXO mailbox
206///
207/// It is tied to an expiry UNIX timestamp and is only valid before that time.
208#[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	/// The mailbox ID for which this authorization is signed
239	pub fn mailbox(&self) -> MailboxIdentifier {
240		self.id
241	}
242
243	/// The time at which this authorization expires
244	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	/// Verify the signature for the mailbox and block hash
251	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				// enforce that timestamp is a valid one
275				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
284/// Convert the raw x,y coordinate pair into a [PublicKey]
285fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
286	//TODO(stevenroose) try to get an official api for this
287	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		// an old one
329		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()); // typo
368		assert!(MailboxType::from_str("").is_err());
369		assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); // case-sensitive
370	}
371}
372