Skip to content

Commit d544840

Browse files
authored
Merge pull request #20 from zig-bitcoin/hrp-networks
bech32: add more utils to hrp and tests
2 parents 6afd168 + 8d42fd3 commit d544840

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed

src/bech32/hrp.zig

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const std = @import("std");
22
const expect = std.testing.expect;
33
const expectEqualSlices = std.testing.expectEqualSlices;
44
const expectError = std.testing.expectError;
5+
const expectEqualStrings = std.testing.expectEqualStrings;
56

67
/// The human readable part of a bech32 address is limited to 83 US-ASCII characters.
78
const MAX_HRP_LEN: usize = 83;
@@ -12,6 +13,60 @@ const MIN_ASCII: u8 = 33;
1213
/// The maximum ASCII value for a valid character in the human readable part.
1314
const MAX_ASCII: u8 = 126;
1415

16+
/// The human-readable part (HRP) for the Bitcoin mainnet.
17+
///
18+
/// This corresponds to `bc` prefix.
19+
///
20+
/// Example:
21+
/// - Mainnet P2WPKH: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
22+
const BC: Hrp = .{
23+
.buf = [_]u8{
24+
98, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
25+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
26+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
27+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
28+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
29+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
30+
0, 0, 0, 0, 0,
31+
},
32+
.size = 2,
33+
};
34+
35+
/// The human-readable part (HRP) for Bitcoin testnet networks (testnet and signet).
36+
///
37+
/// This corresponds to `tb` prefix.
38+
///
39+
/// Example:
40+
/// - Testnet P2WPKH: tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx
41+
const TB: Hrp = .{
42+
.buf = [_]u8{
43+
116, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
44+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
45+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
46+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
47+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
48+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
49+
0, 0, 0, 0, 0,
50+
},
51+
.size = 2,
52+
};
53+
54+
/// The human-readable part (HRP) for the Bitcoin regtest network.
55+
///
56+
/// This corresponds to `bcrt` prefix.
57+
const BCRT: Hrp = .{
58+
.buf = [_]u8{
59+
98, 99, 114, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0,
60+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
61+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
62+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
63+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
64+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
65+
0, 0, 0, 0, 0,
66+
},
67+
.size = 4,
68+
};
69+
1570
/// Various errors that can occur during HRP processing.
1671
///
1772
/// These errors help in validating and debugging issues with bech32 address formatting.
@@ -146,6 +201,110 @@ pub const Hrp = struct {
146201
// Return the constructed and validated `Hrp` instance.
147202
return new;
148203
}
204+
205+
/// Converts the human-readable part (HRP) to a lowercase representation.
206+
pub fn toLowerCase(self: *const Self, output: []u8) []const u8 {
207+
std.debug.assert(output.len >= self.size);
208+
209+
// Loop through each character of the HRP and convert it to lowercase.
210+
for (self.buf[0..self.size], 0..) |b, i| {
211+
output[i] = std.ascii.toLower(b);
212+
}
213+
214+
return output[0..self.size];
215+
}
216+
217+
/// Converts the human-readable part (HRP) to bytes.
218+
pub fn asBytes(self: *const Self) []const u8 {
219+
return self.buf[0..self.size];
220+
}
221+
222+
/// Checks whether two HRPs are equal.
223+
pub fn eql(self: *const Self, rhs: *const Self) bool {
224+
// If the HRPs have different sizes, they are not equal.
225+
if (self.size != rhs.size) return false;
226+
227+
// Create buffers to store the lowercase versions of the HRPs.
228+
var buf_lhs: [MAX_HRP_LEN]u8 = undefined;
229+
var buf_rhs: [MAX_HRP_LEN]u8 = undefined;
230+
231+
// Convert both HRPs to lowercase.
232+
const l = self.toLowerCase(&buf_lhs);
233+
const r = rhs.toLowerCase(&buf_rhs);
234+
235+
// Compare each byte of the lowercase HRPs for equality.
236+
for (l, r) |a, b|
237+
if (a != b) return false;
238+
239+
return true;
240+
}
241+
242+
/// Checks whether a given Segwit address is valid on either the mainnet or testnet.
243+
///
244+
/// A Segwit address must follow the Bech32 encoding format, with the human-readable
245+
/// part "bc" for mainnet or "tb" for testnet. This function combines the logic of
246+
/// validating an address on both networks.
247+
///
248+
/// # Returns
249+
/// - `true` if the Segwit address is valid on either the mainnet or testnet.
250+
/// - `false` otherwise.
251+
///
252+
/// # Segwit Address Requirements:
253+
/// - The human-readable part must be "bc" (mainnet) or "tb" (testnet).
254+
/// - The witness program must follow the rules outlined in BIP141.
255+
pub fn isValidSegwit(self: *const Self) bool {
256+
return self.isValidOnMainnet() or self.isValidOnTestnet();
257+
}
258+
259+
/// Checks whether a given Segwit address is valid on the Bitcoin mainnet.
260+
///
261+
/// Segwit addresses on the mainnet use the human-readable part "bc". This function
262+
/// verifies that the provided address corresponds to the mainnet format.
263+
///
264+
/// # Returns
265+
/// - `true` if the Segwit address is valid on the mainnet (with the "bc" prefix).
266+
/// - `false` otherwise.
267+
pub fn isValidOnMainnet(self: *const Self) bool {
268+
return self.eql(&BC);
269+
}
270+
271+
/// Checks whether a given Segwit address is valid on the Bitcoin testnet.
272+
///
273+
/// Segwit addresses on the testnet use the human-readable part "tb". This function
274+
/// verifies that the provided address corresponds to the testnet format.
275+
///
276+
/// # Returns
277+
/// - `true` if the Segwit address is valid on the testnet (with the "tb" prefix).
278+
/// - `false` otherwise.
279+
pub fn isValidOnTestnet(self: *const Self) bool {
280+
return self.eql(&TB);
281+
}
282+
283+
/// Checks whether a given Segwit address is valid on the Bitcoin signet.
284+
///
285+
/// Segwit addresses on signet also use the human-readable part "tb", similar to
286+
/// testnet addresses. This function verifies that the provided address corresponds
287+
/// to the signet format.
288+
///
289+
/// # Returns
290+
/// - `true` if the Segwit address is valid on signet (with the "tb" prefix).
291+
/// - `false` otherwise.
292+
pub fn isValidOnSignet(self: *const Self) bool {
293+
return self.eql(&TB);
294+
}
295+
296+
/// Checks whether a given Segwit address is valid on the Bitcoin regtest network.
297+
///
298+
/// Segwit addresses on the regtest network use the human-readable part "bcrt".
299+
/// This function verifies that the provided address corresponds to the regtest
300+
/// format.
301+
///
302+
/// # Returns
303+
/// - `true` if the Segwit address is valid on regtest (with the "bcrt" prefix).
304+
/// - `false` otherwise.
305+
pub fn isValidOnRegtest(self: *const Self) bool {
306+
return self.eql(&BCRT);
307+
}
149308
};
150309

151310
test "Hrp: check parse is ok" {
@@ -210,3 +369,91 @@ test "Hrp: Hrp with invalid ASCII byte should fail parsing" {
210369
// Attempt to parse the HRP with invalid characters, expecting an `InvalidAsciiByte` error.
211370
try expectError(HrpError.InvalidAsciiByte, Hrp.parse(case));
212371
}
372+
373+
test "Hrp: Hrp to lower case" {
374+
// Some valid human readable parts.
375+
const cases = [_][]const u8{
376+
"a",
377+
"A",
378+
"abcdefg",
379+
"ABCDEFG",
380+
"abc123def",
381+
"ABC123DEF",
382+
"!\"#$%&'()*+,-./",
383+
"1234567890",
384+
};
385+
386+
// The expected results for the human readable parts in lowercase.
387+
const expected_results = [_][]const u8{
388+
"a",
389+
"a",
390+
"abcdefg",
391+
"abcdefg",
392+
"abc123def",
393+
"abc123def",
394+
"!\"#$%&'()*+,-./",
395+
"1234567890",
396+
};
397+
398+
// Go through all the test cases.
399+
for (cases, expected_results) |case, expected| {
400+
// Parse the human readable part.
401+
const hrp = try Hrp.parse(case);
402+
var buf: [MAX_HRP_LEN]u8 = undefined;
403+
404+
// Convert the human readable part to lowercase.
405+
try expectEqualStrings(expected, hrp.toLowerCase(&buf));
406+
}
407+
}
408+
409+
test "Hrp: as bytes should return the proper bytes" {
410+
// Some valid human readable parts.
411+
const cases = [_][]const u8{
412+
"a",
413+
"A",
414+
"abcdefg",
415+
"ABCDEFG",
416+
"abc123def",
417+
"ABC123DEF",
418+
"!\"#$%&'()*+,-./",
419+
"1234567890",
420+
};
421+
422+
// Go through all the test cases.
423+
for (cases) |case| {
424+
// Parse the human readable part.
425+
const hrp = try Hrp.parse(case);
426+
// Convert the human readable part to lowercase.
427+
try expectEqualSlices(u8, case, hrp.asBytes());
428+
}
429+
}
430+
431+
test "Hrp: ensure eql function works properly" {
432+
// Parse two human readable parts which are equal.
433+
const lhs1 = try Hrp.parse("!\"#$%&'()*+,-./");
434+
const rhs1 = try Hrp.parse("!\"#$%&'()*+,-./");
435+
// Assert that the two human readable parts are equal.
436+
try expect(lhs1.eql(&rhs1));
437+
438+
// Generate another human readable part which is different.
439+
const rhs2 = try Hrp.parse("!\"#$%&'()*+,-.a");
440+
// Assert that the two human readable parts are not equal.
441+
try expect(!lhs1.eql(&rhs2));
442+
443+
// Generate another human readable part with a different size.
444+
const rhs3 = try Hrp.parse("!\"#$%&'()*+,-.");
445+
// Assert that the two human readable parts are not equal (different size).
446+
try expect(!lhs1.eql(&rhs3));
447+
448+
// Parse two human readable parts which are equal, but with different case.
449+
const lhs_case_insensitive = try Hrp.parse("abcdefg");
450+
const rhs_case_insensitive = try Hrp.parse("ABCDEFG");
451+
// Assert that the two human readable parts are equal.
452+
try expect(lhs_case_insensitive.eql(&rhs_case_insensitive));
453+
}
454+
455+
test "Hrp: ensure constants are properly setup" {
456+
try expect(BC.eql(&(try Hrp.parse("bc"))));
457+
try expect(TB.eql(&(try Hrp.parse("tb"))));
458+
try expect(BCRT.eql(&(try Hrp.parse("bcrt"))));
459+
}

0 commit comments

Comments
 (0)