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		Ok(Self(r.read_byte_array()?))
142	}
143}
144
145/// Blinded identifier for a mailbox
146///
147/// It is blinded by adding to the mailbox public key point the
148/// Diffie-Hellman secret between the server's key and the VTXO key from
149/// the address.
150///
151/// Represented as a curve point.
152#[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	/// Convert to public key
171	pub fn as_pubkey(&self) -> PublicKey {
172		PublicKey::from_slice(&self.0).expect("invalid pubkey")
173	}
174
175	/// Convert from a public key
176	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/// Authorization to read a VTXO mailbox
198///
199/// It is tied to an expiry UNIX timestamp and is only valid before that time.
200#[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	/// The mailbox ID for which this authorization is signed
231	pub fn mailbox(&self) -> MailboxIdentifier {
232		self.id
233	}
234
235	/// The time at which this authorization expires
236	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	/// Verify the signature for the mailbox and block hash
243	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				// enforce that timestamp is a valid one
267				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
276/// Convert the raw x,y coordinate pair into a [PublicKey]
277fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
278	//TODO(stevenroose) try to get an official api for this
279	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		// an old one
321		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()); // typo
360		assert!(MailboxType::from_str("").is_err());
361		assert!(MailboxType::from_str("ARKOOR_RECEIVE").is_err()); // case-sensitive
362	}
363}
364