Skip to content

Commit 7f610cb

Browse files
committed
Store HumanReadableNames in-object rather than on the heap
Since the full encoded domain name of an HRN cannot exceed the maximum length of a DNS name (255 octets), there's not a lot of reason to store the `user` and `domain` parts of an HRN on the heap via two `String`s. Instead, here, we store one byte array with the maximum size of both labels as well as the length of the `user` and `domain` parts. Because we're now avoiding heap allocations this also implies making `HumanReadableName::new` take the `user` and `domain` parts by reference as `&str`s, rather than by value as `String`s.
1 parent 83e9e80 commit 7f610cb

File tree

1 file changed

+45
-28
lines changed

1 file changed

+45
-28
lines changed

lightning/src/onion_message/dns_resolution.rs

+45-28
Original file line numberDiff line numberDiff line change
@@ -179,42 +179,58 @@ impl OnionMessageContents for DNSResolverMessage {
179179
}
180180
}
181181

182+
// Note that `REQUIRED_EXTRA_LEN` includes the (implicit) trailing `.`
183+
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
184+
182185
/// A struct containing the two parts of a BIP 353 Human Readable Name - the user and domain parts.
183186
///
184-
/// The `user` and `domain` parts, together, cannot exceed 232 bytes in length, and both must be
187+
/// The `user` and `domain` parts, together, cannot exceed 231 bytes in length, and both must be
185188
/// non-empty.
186189
///
187-
/// To protect against [Homograph Attacks], both parts of a Human Readable Name must be plain
188-
/// ASCII.
190+
/// If you intend to handle non-ASCII `user` or `domain` parts, you must handle [Homograph Attacks]
191+
/// and do punycode en-/de-coding yourself. This struc will always handle only plain ASCII `user`
192+
/// and `domain` parts.
193+
///
194+
/// This struct can also be used for LN-Address recipients.
189195
///
190196
/// [Homograph Attacks]: https://en.wikipedia.org/wiki/IDN_homograph_attack
191197
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
192198
pub struct HumanReadableName {
193-
// TODO Remove the heap allocations given the whole data can't be more than 256 bytes.
194-
user: String,
195-
domain: String,
199+
contents: [u8; 255 - REQUIRED_EXTRA_LEN],
200+
user_len: u8,
201+
domain_len: u8,
202+
}
203+
204+
/// Check if the chars in `s` are allowed to be included in a hostname.
205+
pub(crate) fn str_chars_allowed(s: &str) -> bool {
206+
s.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-')
196207
}
197208

198209
impl HumanReadableName {
199210
/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
200211
/// struct-level documentation for more on the requirements on each.
201-
pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
212+
pub fn new(user: &str, mut domain: &str) -> Result<HumanReadableName, ()> {
202213
// First normalize domain and remove the optional trailing `.`
203-
if domain.ends_with(".") {
204-
domain.pop();
214+
if domain.ends_with('.') {
215+
domain = &domain[..domain.len() - 1];
205216
}
206-
// Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.`
207-
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
208217
if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
209218
return Err(());
210219
}
211220
if user.is_empty() || domain.is_empty() {
212221
return Err(());
213222
}
214-
if !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) {
223+
if !str_chars_allowed(&user) || !str_chars_allowed(&domain) {
215224
return Err(());
216225
}
217-
Ok(HumanReadableName { user, domain })
226+
let mut contents = [0; 255 - REQUIRED_EXTRA_LEN];
227+
contents[..user.len()].copy_from_slice(user.as_bytes());
228+
contents[user.len()..user.len() + domain.len()].copy_from_slice(domain.as_bytes());
229+
Ok(HumanReadableName {
230+
contents,
231+
user_len: user.len() as u8,
232+
domain_len: domain.len() as u8,
233+
})
218234
}
219235

220236
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
@@ -224,49 +240,50 @@ impl HumanReadableName {
224240
pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
225241
if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
226242
{
227-
Self::new(user.to_string(), domain.to_string())
243+
Self::new(user, domain)
228244
} else {
229245
Err(())
230246
}
231247
}
232248

233249
/// Gets the `user` part of this Human Readable Name
234250
pub fn user(&self) -> &str {
235-
&self.user
251+
let bytes = &self.contents[..self.user_len as usize];
252+
core::str::from_utf8(bytes).expect("Checked in constructor")
236253
}
237254

238255
/// Gets the `domain` part of this Human Readable Name
239256
pub fn domain(&self) -> &str {
240-
&self.domain
257+
let user_len = self.user_len as usize;
258+
let bytes = &self.contents[user_len..user_len + self.domain_len as usize];
259+
core::str::from_utf8(bytes).expect("Checked in constructor")
241260
}
242261
}
243262

244263
// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request`
245264
impl Writeable for HumanReadableName {
246265
fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
247-
(self.user.len() as u8).write(writer)?;
248-
writer.write_all(&self.user.as_bytes())?;
249-
(self.domain.len() as u8).write(writer)?;
250-
writer.write_all(&self.domain.as_bytes())
266+
(self.user().len() as u8).write(writer)?;
267+
writer.write_all(&self.user().as_bytes())?;
268+
(self.domain().len() as u8).write(writer)?;
269+
writer.write_all(&self.domain().as_bytes())
251270
}
252271
}
253272

254273
impl Readable for HumanReadableName {
255274
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
256-
let mut read_bytes = [0; 255];
257-
275+
let mut user_bytes = [0; 255];
258276
let user_len: u8 = Readable::read(reader)?;
259-
reader.read_exact(&mut read_bytes[..user_len as usize])?;
260-
let user_bytes: Vec<u8> = read_bytes[..user_len as usize].into();
261-
let user = match String::from_utf8(user_bytes) {
277+
reader.read_exact(&mut user_bytes[..user_len as usize])?;
278+
let user = match core::str::from_utf8(&user_bytes[..user_len as usize]) {
262279
Ok(user) => user,
263280
Err(_) => return Err(DecodeError::InvalidValue),
264281
};
265282

283+
let mut domain_bytes = [0; 255];
266284
let domain_len: u8 = Readable::read(reader)?;
267-
reader.read_exact(&mut read_bytes[..domain_len as usize])?;
268-
let domain_bytes: Vec<u8> = read_bytes[..domain_len as usize].into();
269-
let domain = match String::from_utf8(domain_bytes) {
285+
reader.read_exact(&mut domain_bytes[..domain_len as usize])?;
286+
let domain = match core::str::from_utf8(&domain_bytes[..domain_len as usize]) {
270287
Ok(domain) => domain,
271288
Err(_) => return Err(DecodeError::InvalidValue),
272289
};

0 commit comments

Comments
 (0)