1//! multibase decoder
2//!
3//! decodes multibase-encoded strings (prefix + encoded data).
4//! currently supports base58btc (z prefix) for DID document public keys.
5//!
6//! see: https://github.com/multiformats/multibase
7
8const std = @import("std");
9
10/// multibase encoding types
11pub const Encoding = enum {
12 base58btc, // z prefix
13
14 pub fn fromPrefix(prefix: u8) ?Encoding {
15 return switch (prefix) {
16 'z' => .base58btc,
17 else => null,
18 };
19 }
20};
21
22/// decode a multibase string, returning the raw bytes
23/// the first character is the encoding prefix
24pub fn decode(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
25 if (input.len == 0) return error.EmptyInput;
26
27 const encoding = Encoding.fromPrefix(input[0]) orelse return error.UnsupportedEncoding;
28
29 return switch (encoding) {
30 .base58btc => try base58btc.decode(allocator, input[1..]),
31 };
32}
33
34/// base58btc decoder (bitcoin alphabet)
35pub const base58btc = struct {
36 /// bitcoin base58 alphabet
37 const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
38
39 /// reverse lookup table
40 const decode_table: [256]i8 = blk: {
41 var table: [256]i8 = .{-1} ** 256;
42 for (alphabet, 0..) |c, i| {
43 table[c] = @intCast(i);
44 }
45 break :blk table;
46 };
47
48 /// decode base58btc string to bytes
49 pub fn decode(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
50 if (input.len == 0) return allocator.alloc(u8, 0);
51
52 // count leading zeros (1s in base58)
53 var leading_zeros: usize = 0;
54 for (input) |c| {
55 if (c != '1') break;
56 leading_zeros += 1;
57 }
58
59 // estimate output size: each base58 char represents ~5.86 bits
60 // use a simple overestimate: input.len bytes is more than enough
61 const max_output = input.len;
62 const result = try allocator.alloc(u8, max_output);
63 errdefer allocator.free(result);
64
65 // decode using big integer arithmetic
66 // accumulator = accumulator * 58 + digit
67 var acc = try std.math.big.int.Managed.init(allocator);
68 defer acc.deinit();
69
70 var multiplier = try std.math.big.int.Managed.initSet(allocator, @as(u64, 58));
71 defer multiplier.deinit();
72
73 var temp = try std.math.big.int.Managed.init(allocator);
74 defer temp.deinit();
75
76 for (input) |c| {
77 const digit = decode_table[c];
78 if (digit < 0) {
79 allocator.free(result);
80 return error.InvalidCharacter;
81 }
82
83 // acc = acc * 58 + digit
84 try temp.mul(&acc, &multiplier);
85 try acc.copy(temp.toConst());
86 try acc.addScalar(&acc, @as(u8, @intCast(digit)));
87 }
88
89 // convert big int to bytes (big-endian for base58)
90 const limbs = acc.toConst().limbs;
91 const limb_count = acc.len();
92
93 // calculate byte size from limbs
94 var byte_count: usize = 0;
95 if (limb_count > 0 and !acc.toConst().eqlZero()) {
96 const bit_count = acc.toConst().bitCountAbs();
97 byte_count = (bit_count + 7) / 8;
98 }
99
100 // write bytes in big-endian order
101 var output_bytes = try allocator.alloc(u8, leading_zeros + byte_count);
102 errdefer allocator.free(output_bytes);
103
104 // leading zeros
105 @memset(output_bytes[0..leading_zeros], 0);
106
107 // convert limbs to big-endian bytes
108 if (byte_count > 0) {
109 const output_slice = output_bytes[leading_zeros..];
110
111 // limbs are in little-endian order, we need big-endian output
112 var pos: usize = byte_count;
113 for (limbs[0..limb_count]) |limb| {
114 const limb_bytes = @sizeOf(@TypeOf(limb));
115 var i: usize = 0;
116 while (i < limb_bytes and pos > 0) : (i += 1) {
117 pos -= 1;
118 output_slice[pos] = @truncate(limb >> @intCast(i * 8));
119 }
120 }
121 }
122
123 allocator.free(result);
124 return output_bytes;
125 }
126};
127
128// === tests ===
129
130test "base58btc decode" {
131 const alloc = std.testing.allocator;
132
133 // "abc" in base58btc
134 // "abc" = 0x616263 = 6382179
135 // expected base58btc: "ZiCa" (verify with external tool)
136 {
137 const decoded = try base58btc.decode(alloc, "ZiCa");
138 defer alloc.free(decoded);
139 try std.testing.expectEqualSlices(u8, "abc", decoded);
140 }
141}
142
143test "base58btc decode with leading zeros" {
144 const alloc = std.testing.allocator;
145
146 // leading 1s map to leading zero bytes
147 {
148 const decoded = try base58btc.decode(alloc, "111");
149 defer alloc.free(decoded);
150 try std.testing.expectEqual(@as(usize, 3), decoded.len);
151 try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0, 0 }, decoded);
152 }
153}
154
155test "multibase decode base58btc" {
156 const alloc = std.testing.allocator;
157
158 // z prefix = base58btc
159 {
160 const decoded = try decode(alloc, "zZiCa");
161 defer alloc.free(decoded);
162 try std.testing.expectEqualSlices(u8, "abc", decoded);
163 }
164}
165
166test "base58btc decode real multibase key - secp256k1" {
167 const alloc = std.testing.allocator;
168 const multicodec = @import("multicodec.zig");
169
170 // from a real DID document: zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF
171 // this is a compressed secp256k1 public key with multicodec prefix
172 const key = "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF";
173 const decoded = try decode(alloc, key);
174 defer alloc.free(decoded);
175
176 // should decode to 35 bytes: 2-byte multicodec prefix (0xe7 0x01 varint) + 33-byte compressed key
177 try std.testing.expectEqual(@as(usize, 35), decoded.len);
178
179 // first two bytes should be secp256k1-pub multicodec prefix (0xe7 0x01 varint for 231)
180 try std.testing.expectEqual(@as(u8, 0xe7), decoded[0]);
181 try std.testing.expectEqual(@as(u8, 0x01), decoded[1]);
182
183 // parse with multicodec
184 const parsed = try multicodec.parsePublicKey(decoded);
185 try std.testing.expectEqual(multicodec.KeyType.secp256k1, parsed.key_type);
186 try std.testing.expectEqual(@as(usize, 33), parsed.raw.len);
187
188 // compressed point prefix should be 0x02 or 0x03
189 try std.testing.expect(parsed.raw[0] == 0x02 or parsed.raw[0] == 0x03);
190}