Using Bech32 and Bech32m
Bech32 implements the checksummed base-32 format defined by BIP 173
(Bech32) and BIP 350 (Bech32m). It is best known
as the encoding of Bitcoin SegWit addresses (bc1…) and Lightning BOLT11 invoices, but it is a general-purpose
encoding for any payload that benefits from a human-readable prefix and strong, position-aware error detection.
Unlike the other encodings in this library, Bech32 is not a flat binary-to-text transform. An encoded string has four parts:
bc 1 qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
│ │ │ │
│ │ │ └─ 6-symbol checksum
│ │ └─ data part (5-bit groups, drawn from "qpzry9x8gf2tvdw0s3jn54khce6mua7l")
│ └─ separator '1'
└─ human-readable part (HRP)
Because the HRP and checksum are integral to the string, Bech32 is modelled on Base58Check
rather than the IBinaryEncoding family - it sits outside the runtime registry.
5-bit groups vs. 8-bit bytes
The core methods operate on 5-bit data groups (each value 0-31). Two convenience pairs bridge to ordinary
bytes, and ConvertBits does it by hand:
| Method | Data form |
|---|---|
Encode(hrp, data, scheme) / Decode(...) |
5-bit groups (values 0-31) |
EncodeFromBytes(hrp, data, scheme) / DecodeToBytes(...) |
8-bit bytes (repacked with ConvertBits internally) |
ConvertBits(data, fromBits, toBits, pad) |
manual bit-width conversion |
using Bodu.Text.Encoding;
// Round-trip arbitrary bytes through the byte-oriented pair.
byte[] payload = { 0xDE, 0xAD, 0xBE, 0xEF };
string encoded = Bech32.EncodeFromBytes("data", payload, Bech32Encoding.Bech32m);
Bech32.DecodeToBytes(encoded, out string hrp, out byte[] back, out Bech32Encoding scheme);
// hrp == "data"; back.SequenceEqual(payload); scheme == Bech32Encoding.Bech32m
Bech32 vs. Bech32m
The two schemes differ only in the checksum constant, which makes a Bech32m string fail a Bech32 checksum and vice versa. Bech32Encoding selects the scheme on encode, and the decoder reports which scheme validated the checksum through an out parameter:
| Scheme | BIP | Use |
|---|---|---|
Bech32Encoding.Bech32 (default) |
BIP 173 | SegWit v0 (bc1q…), Lightning invoices |
Bech32Encoding.Bech32m |
BIP 350 | SegWit v1+ / Taproot (bc1p…) |
Bech32.Decode("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
out string hrp, out byte[] data, out Bech32Encoding scheme);
// scheme == Bech32Encoding.Bech32
Worked example - a SegWit v0 address
A SegWit address is not a plain byte payload: the data part is a one-symbol witness version followed by the witness program repacked from 8 bits to 5. Witness v0 uses Bech32; v1+ uses Bech32m.
using Bodu.Text.Encoding;
byte[] program = Base16.Decode("751e76e8199196d454941c45d1b3a323f1433bd6"); // 20-byte HASH160
byte[] groups = Bech32.ConvertBits(program, 8, 5, pad: true)!; // program → 5-bit groups
byte[] data = new byte[1 + groups.Length];
data[0] = 0; // witness version 0
groups.CopyTo(data, 1);
string address = Bech32.Encode("bc", data, Bech32Encoding.Bech32);
// "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
Case handling
Encoded output is always lower case (the canonical form). The decoder accepts an all-lower-case or all-upper-case string and rejects mixed case per BIP 173; the returned HRP is normalised to lower case.
Bech32.IsValid("BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"); // true (all upper)
Bech32.IsValid("bc1qw508d6qejxtdg4Y5r3zarvary0c5xw7kv8f3t4"); // false (mixed case)
The 90-character limit
BIP 173 caps an address at 90 characters. The decoder enforces this limit; the encoder does not, so it can produce the longer strings non-address schemes need - Lightning BOLT11 invoices routinely exceed 90 characters.
HRP rules
The human-readable part must be non-empty and contain only US-ASCII characters in the range 33-126. An empty or
out-of-range HRP throws ArgumentException from the encoder; TryEncode returns false.
Non-throwing forms
if (Bech32.TryEncode("bc", data, Bech32Encoding.Bech32, out string? encoded))
{
// encoded is non-null
}
if (Bech32.TryDecodeToBytes(input, out string? hrp, out byte[]? bytes, out Bech32Encoding scheme))
{
// hrp / bytes are non-null; scheme reports Bech32 vs Bech32m
}
Decode / DecodeToBytes throw FormatException on a bad checksum, mixed case, missing separator, over-length
input, or an out-of-alphabet character; the Try* forms return false instead. Bech32.IsValid is a shorthand for
"does this decode without error".
Sizing
// hrpLength + 1 separator + dataLength + 6 checksum symbols
int length = Bech32.GetEncodedLength(hrpLength: 2, dataLength: 33);
Where to go next
- Base58 guide - the legacy Bitcoin address encoding and
Base58Check. - Base32 guide - the plain 5-bit encoding Bech32's data part is built on.
IBinaryEncodinginterface - the runtime registry for the flat-byte encodings (Bech32 stays outside it).- Text & Serialization guides - every guide in this topic, across Bodu.Text.Encoding, Bodu.Text.Formats, and the Bencode / TOML serializers.