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