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::io;
6
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/// Identifier for a mailbox
15///
16/// Represented as a curve point.
17#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct MailboxIdentifier([u8; PUBLIC_KEY_SIZE]);
19
20impl_byte_newtype!(MailboxIdentifier, PUBLIC_KEY_SIZE);
21
22impl MailboxIdentifier {
23	/// Convert to public key
24	pub fn as_pubkey(&self) -> PublicKey {
25		PublicKey::from_slice(&self.0).expect("invalid pubkey")
26	}
27
28	/// Convert from a public key
29	pub fn from_pubkey(pubkey: PublicKey) -> Self {
30		Self(pubkey.serialize())
31	}
32
33	/// Blind the mailbox id with the server pubkey and the VTXO privkey
34	pub fn to_blinded(
35		&self,
36		server_pubkey: PublicKey,
37		vtxo_key: &Keypair,
38	) -> BlindedMailboxIdentifier {
39		BlindedMailboxIdentifier::new(*self, server_pubkey, vtxo_key)
40	}
41
42	/// Unblind a blinded mailbox identifier
43	pub fn from_blinded(
44		blinded: BlindedMailboxIdentifier,
45		vtxo_pubkey: PublicKey,
46		server_key: &Keypair,
47	) -> MailboxIdentifier {
48		let dh = ecdh::shared_secret_point(&vtxo_pubkey, &server_key.secret_key());
49		let neg_dh_pk = point_to_pubkey(&dh).negate(&SECP);
50		let ret = PublicKey::combine_keys(&[&blinded.as_pubkey(), &neg_dh_pk])
51			.expect("error adding DH secret to mailbox key");
52		Self(ret.serialize())
53	}
54}
55
56impl From<PublicKey> for MailboxIdentifier {
57	fn from(pk: PublicKey) -> Self {
58		Self::from_pubkey(pk)
59	}
60}
61
62impl ProtocolEncoding for MailboxIdentifier {
63	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
64		w.emit_slice(self.as_ref())
65	}
66
67	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
68		Ok(Self(r.read_byte_array()?))
69	}
70}
71
72/// Blinded identifier for a mailbox
73///
74/// It is blinded by adding to the mailbox public key point the
75/// Diffie-Hellman secret between the server's key and the VTXO key from
76/// the address.
77///
78/// Represented as a curve point.
79#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
80pub struct BlindedMailboxIdentifier([u8; PUBLIC_KEY_SIZE]);
81
82impl_byte_newtype!(BlindedMailboxIdentifier, PUBLIC_KEY_SIZE);
83
84impl BlindedMailboxIdentifier {
85	pub fn new(
86		mailbox_id: MailboxIdentifier,
87		server_pubkey: PublicKey,
88		vtxo_key: &Keypair,
89	) -> BlindedMailboxIdentifier {
90		let dh = ecdh::shared_secret_point(&server_pubkey, &vtxo_key.secret_key());
91		let dh_pk = point_to_pubkey(&dh);
92		let ret = PublicKey::combine_keys(&[&mailbox_id.as_pubkey(), &dh_pk])
93			.expect("error adding DH secret to mailbox key");
94		Self(ret.serialize())
95	}
96
97	/// Convert to public key
98	pub fn as_pubkey(&self) -> PublicKey {
99		PublicKey::from_slice(&self.0).expect("invalid pubkey")
100	}
101
102	/// Convert from a public key
103	pub fn from_pubkey(pubkey: PublicKey) -> Self {
104		Self(pubkey.serialize())
105	}
106}
107
108impl From<PublicKey> for BlindedMailboxIdentifier {
109	fn from(pk: PublicKey) -> Self {
110		Self::from_pubkey(pk)
111	}
112}
113
114impl ProtocolEncoding for BlindedMailboxIdentifier {
115	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
116		w.emit_slice(self.as_ref())
117	}
118
119	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
120		Ok(Self(r.read_byte_array()?))
121	}
122}
123
124/// Authorization to read a VTXO mailbox
125///
126/// It is tied to a block hash and is valid only as long as this block
127/// is recent. Recentness is specified per Ark server, but users are
128/// encouraged to use the tip when creating an authorization.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
130pub struct MailboxAuthorization {
131	id: MailboxIdentifier,
132	expiry: i64,
133	sig: schnorr::Signature,
134}
135
136impl MailboxAuthorization {
137	const CHALENGE_MESSAGE_PREFIX: &'static [u8; 32] = b"Ark VTXO mailbox authorization: ";
138
139	fn signable_message(expiry: i64) -> Message {
140		let mut eng = sha256::Hash::engine();
141		eng.input(Self::CHALENGE_MESSAGE_PREFIX);
142		eng.input(&expiry.to_le_bytes());
143		Message::from_digest(sha256::Hash::from_engine(eng).to_byte_array())
144	}
145
146	pub fn new(
147		mailbox_key: &Keypair,
148		expiry: chrono::DateTime<chrono::Local>,
149	) -> MailboxAuthorization {
150		let expiry = expiry.timestamp();
151		let msg = Self::signable_message(expiry);
152		MailboxAuthorization {
153			id: MailboxIdentifier::from_pubkey(mailbox_key.public_key()),
154			expiry: expiry,
155			sig: SECP.sign_schnorr(&msg, mailbox_key),
156		}
157	}
158
159	/// The mailbox ID for which this authorization is signed
160	pub fn mailbox(&self) -> MailboxIdentifier {
161		self.id
162	}
163
164	/// The time at which this authorization expires
165	pub fn expiry(&self) -> chrono::DateTime<chrono::Local> {
166		chrono::DateTime::from_timestamp_secs(self.expiry)
167			.expect("we guarantee valid timestamp")
168			.with_timezone(&chrono::Local)
169	}
170
171	/// Verify the signature for the mailbox and block hash
172	pub fn verify(&self) -> bool {
173		let msg = Self::signable_message(self.expiry);
174		SECP.verify_schnorr(&self.sig, &msg, &self.id.as_pubkey().into()).is_ok()
175	}
176}
177
178impl ProtocolEncoding for MailboxAuthorization {
179	fn encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<(), io::Error> {
180		self.id.encode(w)?;
181		w.emit_slice(&self.expiry.to_le_bytes())?;
182		self.sig.encode(w)?;
183		Ok(())
184	}
185
186	fn decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, ProtocolDecodingError> {
187		Ok(Self {
188			id: ProtocolEncoding::decode(r)?,
189			expiry: {
190				let timestamp = i64::from_le_bytes(r.read_byte_array()?);
191				// enforce that timestamp is a valid one
192				let _ = chrono::DateTime::from_timestamp_secs(timestamp)
193					.ok_or_else(|| ProtocolDecodingError::invalid("invalid timestamp"))?;
194				timestamp
195			},
196			sig: ProtocolEncoding::decode(r)?,
197		})
198	}
199}
200
201/// Convert the raw x,y coordinate pair into a [PublicKey]
202fn point_to_pubkey(point: &[u8; 64]) -> PublicKey {
203	//TODO(stevenroose) try to get an official api for this
204	let mut uncompressed = [0u8; 65];
205	uncompressed[0] = 0x04;
206	uncompressed[1..].copy_from_slice(point);
207	PublicKey::from_slice(&uncompressed).expect("invalid uncompressed pk")
208}
209
210#[cfg(test)]
211mod test {
212	use std::time::Duration;
213	use bitcoin::secp256k1::rand;
214	use super::*;
215
216	#[test]
217	fn mailbox_blinding() {
218		let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
219		let server_key = Keypair::new(&SECP, &mut rand::thread_rng());
220		let vtxo_key = Keypair::new(&SECP, &mut rand::thread_rng());
221
222		let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
223
224		let blinded = mailbox.to_blinded(server_key.public_key(), &vtxo_key);
225
226		let unblinded = MailboxIdentifier::from_blinded(
227			blinded, vtxo_key.public_key(), &server_key,
228		);
229
230		assert_eq!(unblinded, mailbox);
231	}
232
233	#[test]
234	fn mailbox_authorization() {
235		let mailbox_key = Keypair::new(&SECP, &mut rand::thread_rng());
236		let mailbox = MailboxIdentifier::from_pubkey(mailbox_key.public_key());
237
238		let expiry = chrono::Local::now() + Duration::from_secs(60);
239		let auth = MailboxAuthorization::new(&mailbox_key, expiry);
240		assert_eq!(auth.mailbox(), mailbox);
241		assert!(auth.verify());
242
243		assert_eq!(auth, MailboxAuthorization::deserialize(&auth.serialize()).unwrap());
244
245		// an old one
246		let decoded = MailboxAuthorization::deserialize_hex("023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbeac3380369000000000163b3184156804eb26ffbad964a70840229c4ac80da5da9f9a7557874c45259af48671aa26f567c3c855092c51a1ceeb8a17c7540abe0a50e89866bdb90ece9").unwrap();
247		assert_eq!(decoded.expiry, 1761818819);
248		assert_eq!(decoded.id.to_string(), "023f6712126b93bd479baec93fa4b6e6eb7aa8100b2e818954a351e2eb459ccbea");
249		assert!(decoded.verify());
250	}
251}
252