Skip to content

Commit 75d20e5

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 ebde296 commit 75d20e5

File tree

2 files changed

+102
-5
lines changed

2 files changed

+102
-5
lines changed

lightning/src/onion_message/dns_resolution.rs

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

lightning/src/util/ser.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,16 @@ impl Hostname {
14901490
pub fn len(&self) -> u8 {
14911491
(&self.0).len() as u8
14921492
}
1493+
1494+
/// Check if the chars in `s` are allowed to be included in a [`Hostname`].
1495+
pub(crate) fn str_is_valid_hostname(s: &str) -> bool {
1496+
s.len() <= 255 &&
1497+
s.chars().all(|c|
1498+
c.is_ascii_alphanumeric() ||
1499+
c == '.' ||
1500+
c == '-'
1501+
)
1502+
}
14931503
}
14941504

14951505
impl core::fmt::Display for Hostname {
@@ -1525,11 +1535,7 @@ impl TryFrom<String> for Hostname {
15251535
type Error = ();
15261536

15271537
fn try_from(s: String) -> Result<Self, Self::Error> {
1528-
if s.len() <= 255 && s.chars().all(|c|
1529-
c.is_ascii_alphanumeric() ||
1530-
c == '.' ||
1531-
c == '-'
1532-
) {
1538+
if Hostname::str_is_valid_hostname(&s) {
15331539
Ok(Hostname(s))
15341540
} else {
15351541
Err(())

0 commit comments

Comments
 (0)