Skip to content

Commit efa9d23

Browse files
committed
Add a type to track HumanReadableNames
BIP 353 `HumanReadableName`s are represented as `₿user@domain` and can be resolved using DNS into a `bitcoin:` URI. In the next commit, we will add such a resolver using onion messages to fetch records from the DNS, which will rely on this new type to get name information from outside LDK.
1 parent c310c72 commit efa9d23

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed

lightning/src/onion_message/dns_resolution.rs

+91
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,94 @@ impl OnionMessageContents for DNSResolverMessage {
154154
}
155155
}
156156
}
157+
158+
/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts.
159+
///
160+
/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be
161+
/// non-empty.
162+
///
163+
/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain
164+
/// ASCII.
165+
///
166+
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
167+
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
168+
pub struct HumanReadableName {
169+
// TODO Remove the heap allocations given the whole data can't be more than 256 bytes.
170+
user: String,
171+
domain: String,
172+
}
173+
174+
impl HumanReadableName {
175+
/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
176+
/// struct-level documentation for more on the requirements on each.
177+
pub fn new(user: String, domain: String) -> Result<HumanReadableName, ()> {
178+
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
179+
if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
180+
return Err(());
181+
}
182+
if user.is_empty() || domain.is_empty() {
183+
return Err(());
184+
}
185+
if !user.is_ascii() || !domain.is_ascii() {
186+
return Err(());
187+
}
188+
Ok(HumanReadableName { user, domain })
189+
}
190+
191+
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
192+
///
193+
/// If `user` includes the standard BIP 353 ₿ prefix it is automatically removed as required by
194+
/// BIP 353.
195+
pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
196+
if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
197+
{
198+
Self::new(user.to_string(), domain.to_string())
199+
} else {
200+
Err(())
201+
}
202+
}
203+
204+
/// Gets the `user` part of this Human Readable Name
205+
pub fn user(&self) -> &str {
206+
&self.user
207+
}
208+
209+
/// Gets the `domain` part of this Human Readable Name
210+
pub fn domain(&self) -> &str {
211+
&self.domain
212+
}
213+
}
214+
215+
// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request`
216+
impl Writeable for HumanReadableName {
217+
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
218+
(self.user.len() as u8).write(writer)?;
219+
writer.write_all(&self.user.as_bytes())?;
220+
(self.domain.len() as u8).write(writer)?;
221+
writer.write_all(&self.domain.as_bytes())
222+
}
223+
}
224+
225+
impl Readable for HumanReadableName {
226+
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
227+
let mut read_bytes = [0; 255];
228+
229+
let user_len: u8 = Readable::read(reader)?;
230+
reader.read_exact(&mut read_bytes[..user_len as usize])?;
231+
let user_bytes: Vec<u8> = read_bytes[..user_len as usize].into();
232+
let user = match String::from_utf8(user_bytes) {
233+
Ok(user) => user,
234+
Err(_) => return Err(DecodeError::InvalidValue),
235+
};
236+
237+
let domain_len: u8 = Readable::read(reader)?;
238+
reader.read_exact(&mut read_bytes[..domain_len as usize])?;
239+
let domain_bytes: Vec<u8> = read_bytes[..domain_len as usize].into();
240+
let domain = match String::from_utf8(domain_bytes) {
241+
Ok(domain) => domain,
242+
Err(_) => return Err(DecodeError::InvalidValue),
243+
};
244+
245+
HumanReadableName::new(user, domain).map_err(|()| DecodeError::InvalidValue)
246+
}
247+
}

0 commit comments

Comments
 (0)