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	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/// Identifier for a mailbox
93///
94/// Represented as a curve point.
95#[derive(Clone, Debug, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct MailboxIdentifier(PublicKey);
97
98impl MailboxIdentifier {
99	/// Convert to public key
100	pub fn as_pubkey(&self) -> PublicKey {
101		self.0
102	}
103
104	/// Convert from a public key
105	pub fn from_pubkey(pubkey: PublicKey) -> Self {
106		Self(pubkey)
107	}
108
109	/// Blind the mailbox id with the server pubkey and the VTXO privkey
110	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	/// Unblind a blinded mailbox identifier
119	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/// Blinded identifier for a mailbox
172///
173/// It is blinded by adding to the mailbox public key point the
174/// Diffie-Hellman secret between the server's key and the VTXO key from
175/// the address.
176///
177/// Represented as a curve point.
178#[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	/// Convert to public key
197	pub fn as_pubkey(&self) -> PublicKey {
198		PublicKey::from_slice(&self.0).expect("invalid pubkey")
199	}
200
201	/// Convert from a public key
202	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/// Authorization to read a VTXO mailbox
228///
229/// It is tied to an expiry UNIX timestamp and is only valid before that time.
230#[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	/// The mailbox ID for which this authorization is signed
261	pub fn mailbox(&self) -> MailboxIdentifier {
262		self.id
263	}
264
265	/// The time at which this authorization expires
266	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	/// Verify the signature for the mailbox and block hash
273	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				// enforce that timestamp is a valid one
297				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
306/// Convert the raw x,y coordinate pair into a [PublicKey]
307fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
308	//TODO(stevenroose) try to get an official api for this
309	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		// an old one
351		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()); // typo
392		assert!(MailboxType::from_str("").is_err());
393		assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); // case-sensitive
394	}
395}
396