1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace BitWasp\Bech32; |
4
|
|
|
|
5
|
|
|
use BitWasp\Bech32\Exception\Bech32Exception; |
6
|
|
|
|
7
|
|
|
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; |
8
|
|
|
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; |
9
|
|
|
const CHARKEY_KEY = [ |
10
|
|
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
11
|
|
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
12
|
|
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
13
|
|
|
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, |
14
|
|
|
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, |
15
|
|
|
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, |
16
|
|
|
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, |
17
|
|
|
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 |
18
|
|
|
]; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* @param int[] $values |
22
|
|
|
* @param int $numValues |
23
|
|
|
* @return int |
24
|
|
|
*/ |
25
|
|
|
function polyMod(array $values, $numValues) |
26
|
|
|
{ |
27
|
|
|
$chk = 1; |
28
|
|
|
for ($i = 0; $i < $numValues; $i++) { |
29
|
|
|
$top = $chk >> 25; |
30
|
|
|
$chk = ($chk & 0x1ffffff) << 5 ^ $values[$i]; |
31
|
|
|
|
32
|
|
|
for ($j = 0; $j < 5; $j++) { |
33
|
|
|
$value = (($top >> $j) & 1) ? GENERATOR[$j] : 0; |
34
|
|
|
$chk ^= $value; |
35
|
|
|
} |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
return $chk; |
39
|
|
|
} |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* Expands the human readable part into a character array for checksumming. |
43
|
|
|
* @param string $hrp |
44
|
|
|
* @param int $hrpLen |
45
|
|
|
* @return int[] |
46
|
|
|
*/ |
47
|
|
|
function hrpExpand($hrp, $hrpLen) |
48
|
|
|
{ |
49
|
|
|
$expand1 = []; |
50
|
|
|
$expand2 = []; |
51
|
|
|
for ($i = 0; $i < $hrpLen; $i++) { |
52
|
|
|
$o = \ord($hrp[$i]); |
53
|
|
|
$expand1[] = $o >> 5; |
54
|
|
|
$expand2[] = $o & 31; |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
return \array_merge($expand1, [0], $expand2); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Converts words of $fromBits bits to $toBits bits in size. |
62
|
|
|
* |
63
|
|
|
* @param int[] $data - character array of data to convert |
64
|
|
|
* @param int $inLen - number of elements in array |
65
|
|
|
* @param int $fromBits - word (bit count) size of provided data |
66
|
|
|
* @param int $toBits - requested word size (bit count) |
67
|
|
|
* @param bool $pad - whether to pad (only when encoding) |
68
|
|
|
* @return int[] |
69
|
|
|
* @throws Bech32Exception |
70
|
|
|
*/ |
71
|
|
|
function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true) |
72
|
|
|
{ |
73
|
|
|
$acc = 0; |
74
|
|
|
$bits = 0; |
75
|
|
|
$ret = []; |
76
|
|
|
$maxv = (1 << $toBits) - 1; |
77
|
|
|
$maxacc = (1 << ($fromBits + $toBits - 1)) - 1; |
78
|
|
|
|
79
|
|
|
for ($i = 0; $i < $inLen; $i++) { |
80
|
|
|
$value = $data[$i]; |
81
|
|
|
if ($value < 0 || $value >> $fromBits) { |
82
|
|
|
throw new Bech32Exception('Invalid value for convert bits'); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
$acc = (($acc << $fromBits) | $value) & $maxacc; |
86
|
|
|
$bits += $fromBits; |
87
|
|
|
|
88
|
|
|
while ($bits >= $toBits) { |
89
|
|
|
$bits -= $toBits; |
90
|
|
|
$ret[] = (($acc >> $bits) & $maxv); |
91
|
|
|
} |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
if ($pad) { |
95
|
|
|
if ($bits) { |
96
|
|
|
$ret[] = ($acc << $toBits - $bits) & $maxv; |
97
|
|
|
} |
98
|
|
|
} else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) { |
99
|
|
|
throw new Bech32Exception('Invalid data'); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
return $ret; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @param string $hrp |
107
|
|
|
* @param int[] $convertedDataChars |
108
|
|
|
* @return int[] |
109
|
|
|
*/ |
110
|
|
|
function createChecksum($hrp, array $convertedDataChars) |
111
|
|
|
{ |
112
|
|
|
$values = \array_merge(hrpExpand($hrp, \strlen($hrp)), $convertedDataChars); |
113
|
|
|
$polyMod = polyMod(\array_merge($values, [0, 0, 0, 0, 0, 0]), \count($values) + 6) ^ 1; |
114
|
|
|
$results = []; |
115
|
|
|
for ($i = 0; $i < 6; $i++) { |
116
|
|
|
$results[$i] = ($polyMod >> 5 * (5 - $i)) & 31; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
return $results; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Verifies the checksum given $hrp and $convertedDataChars. |
124
|
|
|
* |
125
|
|
|
* @param string $hrp |
126
|
|
|
* @param int[] $convertedDataChars |
127
|
|
|
* @return bool |
128
|
|
|
*/ |
129
|
|
|
function verifyChecksum($hrp, array $convertedDataChars) |
130
|
|
|
{ |
131
|
|
|
$expandHrp = hrpExpand($hrp, \strlen($hrp)); |
132
|
|
|
$r = \array_merge($expandHrp, $convertedDataChars); |
133
|
|
|
$poly = polyMod($r, \count($r)); |
134
|
|
|
return $poly === 1; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @param string $hrp |
139
|
|
|
* @param array $combinedDataChars |
140
|
|
|
* @return string |
141
|
|
|
*/ |
142
|
|
|
function encode($hrp, array $combinedDataChars) |
143
|
|
|
{ |
144
|
|
|
$checksum = createChecksum($hrp, $combinedDataChars); |
145
|
|
|
$characters = \array_merge($combinedDataChars, $checksum); |
146
|
|
|
|
147
|
|
|
$encoded = []; |
148
|
|
|
for ($i = 0, $n = count($characters); $i < $n; $i++) { |
149
|
|
|
$encoded[$i] = CHARSET[$characters[$i]]; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
return "{$hrp}1" . \implode('', $encoded); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
/** |
156
|
|
|
* @throws Bech32Exception |
157
|
|
|
* @param string $sBech - the bech32 encoded string |
158
|
|
|
* @return array - returns [$hrp, $dataChars] |
159
|
|
|
*/ |
160
|
|
|
function decodeRaw($sBech) |
161
|
|
|
{ |
162
|
|
|
$length = \strlen($sBech); |
163
|
|
|
if ($length < 8) { |
164
|
|
|
throw new Bech32Exception("Bech32 string is too short"); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
$chars = array_values(unpack('C*', $sBech)); |
|
|
|
|
168
|
|
|
|
169
|
|
|
$haveUpper = false; |
170
|
|
|
$haveLower = false; |
171
|
|
|
$positionOne = -1; |
172
|
|
|
|
173
|
|
|
for ($i = 0; $i < $length; $i++) { |
174
|
|
|
$x = $chars[$i]; |
175
|
|
|
if ($x < 33 || $x > 126) { |
176
|
|
|
throw new Bech32Exception('Out of range character in bech32 string'); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
if ($x >= 0x61 && $x <= 0x7a) { |
180
|
|
|
$haveLower = true; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if ($x >= 0x41 && $x <= 0x5a) { |
184
|
|
|
$haveUpper = true; |
185
|
|
|
$x = $chars[$i] = $x + 0x20; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
// find location of last '1' character |
189
|
|
|
if ($x === 0x31) { |
190
|
|
|
$positionOne = $i; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
if ($haveUpper && $haveLower) { |
195
|
|
|
throw new Bech32Exception('Data contains mixture of higher/lower case characters'); |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
if ($positionOne === -1) { |
199
|
|
|
throw new Bech32Exception("Missing separator character"); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
if ($positionOne < 1) { |
203
|
|
|
throw new Bech32Exception("Empty HRP"); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
if (($positionOne + 7) > $length) { |
207
|
|
|
throw new Bech32Exception('Too short checksum'); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
$hrp = \pack("C*", ...\array_slice($chars, 0, $positionOne)); |
211
|
|
|
|
212
|
|
|
$data = []; |
213
|
|
|
for ($i = $positionOne + 1; $i < $length; $i++) { |
214
|
|
|
$data[] = ($chars[$i] & 0x80) ? -1 : CHARKEY_KEY[$chars[$i]]; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
if (!verifyChecksum($hrp, $data)) { |
218
|
|
|
throw new Bech32Exception('Invalid bech32 checksum'); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
return [$hrp, array_slice($data, 0, -6)]; |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
/** |
225
|
|
|
* Validates a bech32 string and returns [$hrp, $dataChars] if |
226
|
|
|
* the conversion was successful. An exception is thrown on invalid |
227
|
|
|
* data. |
228
|
|
|
* |
229
|
|
|
* @param string $sBech - the bech32 encoded string |
230
|
|
|
* @return array - returns [$hrp, $dataChars] |
231
|
|
|
* @throws Bech32Exception |
232
|
|
|
*/ |
233
|
|
|
function decode($sBech) |
234
|
|
|
{ |
235
|
|
|
$length = strlen($sBech); |
236
|
|
|
if ($length > 90) { |
237
|
|
|
throw new Bech32Exception('Bech32 string cannot exceed 90 characters in length'); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
return decodeRaw($sBech); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* @param int $version |
245
|
|
|
* @param string $program |
246
|
|
|
* @throws Bech32Exception |
247
|
|
|
*/ |
248
|
|
|
function validateWitnessProgram($version, $program) |
249
|
|
|
{ |
250
|
|
|
if ($version < 0 || $version > 16) { |
251
|
|
|
throw new Bech32Exception("Invalid witness version"); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$sizeProgram = strlen($program); |
255
|
|
|
if ($version === 0) { |
256
|
|
|
if ($sizeProgram !== 20 && $sizeProgram !== 32) { |
257
|
|
|
throw new Bech32Exception("Invalid size for V0 witness program"); |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
if ($sizeProgram < 2 || $sizeProgram > 40) { |
262
|
|
|
throw new Bech32Exception("Witness program size was out of valid range"); |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param string $hrp - human readable part |
268
|
|
|
* @param int $version - segwit script version |
269
|
|
|
* @param string $program - segwit witness program |
270
|
|
|
* @return string - the encoded address |
271
|
|
|
* @throws Bech32Exception |
272
|
|
|
*/ |
273
|
|
|
function encodeSegwit($hrp, $version, $program) |
274
|
|
|
{ |
275
|
|
|
$version = (int) $version; |
276
|
|
|
validateWitnessProgram($version, $program); |
277
|
|
|
|
278
|
|
|
$programChars = array_values(unpack('C*', $program)); |
|
|
|
|
279
|
|
|
$programBits = convertBits($programChars, count($programChars), 8, 5, true); |
280
|
|
|
$encodeData = array_merge([$version], $programBits); |
281
|
|
|
|
282
|
|
|
return encode($hrp, $encodeData); |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
/** |
286
|
|
|
* @param string $hrp - human readable part |
287
|
|
|
* @param string $bech32 - Bech32 string to be decoded |
288
|
|
|
* @return array - [$version, $program] |
289
|
|
|
* @throws Bech32Exception |
290
|
|
|
*/ |
291
|
|
|
function decodeSegwit($hrp, $bech32) |
292
|
|
|
{ |
293
|
|
|
list ($hrpGot, $data) = decode($bech32); |
294
|
|
|
if ($hrpGot !== $hrp) { |
295
|
|
|
throw new Bech32Exception('Invalid prefix for address'); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$dataLen = count($data); |
299
|
|
|
if ($dataLen === 0 || $dataLen > 65) { |
300
|
|
|
throw new Bech32Exception("Invalid length for segwit address"); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
$decoded = convertBits(array_slice($data, 1), count($data) - 1, 5, 8, false); |
304
|
|
|
$program = pack("C*", ...$decoded); |
305
|
|
|
|
306
|
|
|
validateWitnessProgram($data[0], $program); |
307
|
|
|
|
308
|
|
|
return [$data[0], $program]; |
309
|
|
|
} |
310
|
|
|
|