Skip to content

Commit f49d571

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 4359c89 commit f49d571

File tree

1 file changed

+91
-0
lines changed

1 file changed

+91
-0
lines changed

lightning/src/onion_message/dns_resolution.rs

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

0 commit comments

Comments
 (0)