Skip to content

Commit c6921fa

Browse files
authored
Merge pull request #3733 from TheBlueMatt/2025-04-hrn-no-heap
Store `HumanReadableName`s in-object rather than on the heap
2 parents 6241f2a + 7535a60 commit c6921fa

File tree

1 file changed

+40
-28
lines changed

1 file changed

+40
-28
lines changed

lightning/src/onion_message/dns_resolution.rs

+40-28
Original file line numberDiff line numberDiff line change
@@ -179,32 +179,36 @@ 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 struct 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,
196202
}
197203

198204
impl HumanReadableName {
199205
/// Constructs a new [`HumanReadableName`] from the `user` and `domain` parts. See the
200206
/// struct-level documentation for more on the requirements on each.
201-
pub fn new(user: String, mut domain: String) -> Result<HumanReadableName, ()> {
207+
pub fn new(user: &str, mut domain: &str) -> Result<HumanReadableName, ()> {
202208
// First normalize domain and remove the optional trailing `.`
203-
if domain.ends_with(".") {
204-
domain.pop();
209+
if domain.ends_with('.') {
210+
domain = &domain[..domain.len() - 1];
205211
}
206-
// Note that `REQUIRED_EXTRA_LEN` includes the (now implicit) trailing `.`
207-
const REQUIRED_EXTRA_LEN: usize = ".user._bitcoin-payment.".len() + 1;
208212
if user.len() + domain.len() + REQUIRED_EXTRA_LEN > 255 {
209213
return Err(());
210214
}
@@ -214,7 +218,14 @@ impl HumanReadableName {
214218
if !Hostname::str_is_valid_hostname(&user) || !Hostname::str_is_valid_hostname(&domain) {
215219
return Err(());
216220
}
217-
Ok(HumanReadableName { user, domain })
221+
let mut contents = [0; 255 - REQUIRED_EXTRA_LEN];
222+
contents[..user.len()].copy_from_slice(user.as_bytes());
223+
contents[user.len()..user.len() + domain.len()].copy_from_slice(domain.as_bytes());
224+
Ok(HumanReadableName {
225+
contents,
226+
user_len: user.len() as u8,
227+
domain_len: domain.len() as u8,
228+
})
218229
}
219230

220231
/// Constructs a new [`HumanReadableName`] from the standard encoding - `user`@`domain`.
@@ -224,49 +235,50 @@ impl HumanReadableName {
224235
pub fn from_encoded(encoded: &str) -> Result<HumanReadableName, ()> {
225236
if let Some((user, domain)) = encoded.strip_prefix('₿').unwrap_or(encoded).split_once("@")
226237
{
227-
Self::new(user.to_string(), domain.to_string())
238+
Self::new(user, domain)
228239
} else {
229240
Err(())
230241
}
231242
}
232243

233244
/// Gets the `user` part of this Human Readable Name
234245
pub fn user(&self) -> &str {
235-
&self.user
246+
let bytes = &self.contents[..self.user_len as usize];
247+
core::str::from_utf8(bytes).expect("Checked in constructor")
236248
}
237249

238250
/// Gets the `domain` part of this Human Readable Name
239251
pub fn domain(&self) -> &str {
240-
&self.domain
252+
let user_len = self.user_len as usize;
253+
let bytes = &self.contents[user_len..user_len + self.domain_len as usize];
254+
core::str::from_utf8(bytes).expect("Checked in constructor")
241255
}
242256
}
243257

244258
// Serialized per the requirements for inclusion in a BOLT 12 `invoice_request`
245259
impl Writeable for HumanReadableName {
246260
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())
261+
(self.user().len() as u8).write(writer)?;
262+
writer.write_all(&self.user().as_bytes())?;
263+
(self.domain().len() as u8).write(writer)?;
264+
writer.write_all(&self.domain().as_bytes())
251265
}
252266
}
253267

254268
impl Readable for HumanReadableName {
255269
fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
256-
let mut read_bytes = [0; 255];
257-
270+
let mut user_bytes = [0; 255];
258271
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) {
272+
reader.read_exact(&mut user_bytes[..user_len as usize])?;
273+
let user = match core::str::from_utf8(&user_bytes[..user_len as usize]) {
262274
Ok(user) => user,
263275
Err(_) => return Err(DecodeError::InvalidValue),
264276
};
265277

278+
let mut domain_bytes = [0; 255];
266279
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) {
280+
reader.read_exact(&mut domain_bytes[..domain_len as usize])?;
281+
let domain = match core::str::from_utf8(&domain_bytes[..domain_len as usize]) {
270282
Ok(domain) => domain,
271283
Err(_) => return Err(DecodeError::InvalidValue),
272284
};
@@ -331,7 +343,7 @@ impl OMNameResolver {
331343
&self, payment_id: PaymentId, name: HumanReadableName, entropy_source: &ES,
332344
) -> Result<(DNSSECQuery, DNSResolverContext), ()> {
333345
let dns_name =
334-
Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user, name.domain));
346+
Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user(), name.domain()));
335347
debug_assert!(
336348
dns_name.is_ok(),
337349
"The HumanReadableName constructor shouldn't allow names which are too long"

0 commit comments

Comments
 (0)