Completed
Pull Request — master (#503)
by thomas
21:43
created

Bech32::encode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 11
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace BitWasp\Bitcoin;
4
5
use BitWasp\Bitcoin\Script\WitnessProgram;
6
use BitWasp\Buffertools\Buffer;
7
8
class Bech32
9
{
10
    protected static $CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
11
12
    protected static $CHARSET_REV = [
13
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
14
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15
        -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
16
        15, -1, 10, 17, 21, 20, 26, 30,  7,  5, -1, -1, -1, -1, -1, -1,
17
        -1, 29, -1, 24, 13, 25,  9,  8, 23, -1, 18, 22, 31, 27, 19, -1,
18
        1,  0,  3, 16, 11, 28, 12, 14,  6,  4,  2, -1, -1, -1, -1, -1,
19
        -1, 29, -1, 24, 13, 25,  9,  8, 23, -1, 18, 22, 31, 27, 19, -1,
20
        1,  0,  3, 16, 11, 28, 12, 14,  6,  4,  2, -1, -1, -1, -1, -1
21
    ];
22
23
    /**
24
     * @param int[] $values
25
     * @param int $numValues
26
     * @return int
27
     */
28 46
    public static function polyMod(array $values, $numValues)
29
    {
30 46
        $generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
31 46
        $chk = 1;
32 46
        for ($i = 0; $i < $numValues; $i++) {
33 46
            $top = $chk >> 25;
34 46
            $chk = ($chk & 0x1ffffff) << 5 ^ $values[$i];
35
36 46
            for ($j = 0; $j < 5; $j++) {
37 46
                $value = (($top >> $j) & 1) ? $generator[$j] : 0;
38 46
                $chk ^= $value;
39
            }
40
        }
41
42 46
        return $chk;
43
    }
44
45
    /**
46
     * @param string $hrp
47
     * @param int $hrpLen
48
     * @return array
49
     */
50 46
    public static function hrpExpand($hrp, $hrpLen)
51
    {
52 46
        $expand1 = [];
53 46
        $expand2 = [];
54 46
        for ($i = 0; $i < $hrpLen; $i++) {
55 46
            $o = ord($hrp[$i]);
56 46
            $expand1[] = $o >> 5;
57 46
            $expand2[] = $o & 31;
58
        }
59
60 46
        return array_merge($expand1, [0], $expand2);
61
    }
62
63
    /**
64
     * @param string $hrp
65
     * @param int[] $convertedDataChars
66
     * @return bool
67
     */
68 46
    public static function verifyChecksum($hrp, array $convertedDataChars)
69
    {
70 46
        $expandHrp = self::hrpExpand($hrp, strlen($hrp));
71 46
        $r = array_merge($expandHrp, $convertedDataChars);
72 46
        $poly = self::polyMod($r, count($r));
73 46
        return $poly === 1;
74
    }
75
76
    /**
77
     * @param string $sBech
78
     * @return array
79
     */
80 48
    public static function decodeCheck($sBech)
81
    {
82 48
        $length = strlen($sBech);
83 48
        if ($length > 90) {
84
            throw new \RuntimeException("Bech32 string cannot exceed 90 characters in length");
85
        }
86
87 48
        $chars = array_values(unpack("C*", $sBech));
88
89 48
        $haveUpper = false;
90 48
        $haveLower = false;
91 48
        $positionOne = -1;
92
93 48
        for ($i = 0; $i < $length; $i++) {
94 48
            $x = $chars[$i];
95 48
            if ($x < 33 || $x > 126) {
96
                throw new \RuntimeException("Out of range value for bech");
97
            }
98
99 48
            if ($x >= 0x61 && $x <= 0x7a) {
100 38
                $haveLower = true;
101
            }
102
103 48
            if ($x >= 0x41 && $x <= 0x5a) {
104 12
                $haveUpper = true;
105 12
                $x = $chars[$i] = $x + 0x20;
106
            }
107
108
            // find location of last '1' character
109 48
            if ($x === 0x31) {
110 48
                $positionOne = $i;
111
            }
112
        }
113
114 48
        if ($haveUpper && $haveLower) {
115 2
            throw new \RuntimeException("Data contains mixture of higher/lower case characters");
116
        }
117
118 46
        if ($positionOne < 1 || ($positionOne + 7) > $length) {
119
            throw new \RuntimeException("Invalid location for '1' character");
120
        }
121
122 46
        $hrp = [];
123 46
        for ($i = 0; $i < $positionOne; $i++) {
124 46
            $hrp[$i] = chr($chars[$i]);
125
        }
126
127 46
        $hrp = implode("", $hrp);
128 46
        $data = [];
129 46
        for ($i = $positionOne + 1; $i < $length; $i++) {
130 46
            $data[] = ($chars[$i] & 0x80) ? -1 : self::$CHARSET_REV[$chars[$i]];
131
        }
132
133 46
        if (!self::verifyChecksum($hrp, $data)) {
134 12
            throw new \RuntimeException("Invalid bech32 checksum");
135
        }
136
137 44
        return [$hrp, array_slice($data, 0, -6)];
138
    }
139
140
    /**
141
     * @param array $data
142
     * @param int $inLen
143
     * @param int $fromBits
144
     * @param int $toBits
145
     * @param bool $pad
146
     * @return array
147
     */
148 32
    public static function convertBits(array $data, $inLen, $fromBits, $toBits, $pad = true)
149
    {
150 32
        $acc = 0;
151 32
        $bits = 0;
152 32
        $ret = [];
153 32
        $maxv = (1 << $toBits) - 1;
154 32
        $maxacc = (1 << ($fromBits + $toBits - 1)) - 1;
155
156 32
        for ($i = 0; $i < $inLen; $i++) {
157 32
            $value = $data[$i];
158 32
            if ($value < 0 || $value >> $fromBits) {
159
                throw new \InvalidArgumentException("Invalid value for convert bits");
160
            }
161
162 32
            $acc = (($acc << $fromBits) | $value) & $maxacc;
163 32
            $bits += $fromBits;
164
165 32
            while ($bits >= $toBits) {
166 32
                $bits -= $toBits;
167 32
                $ret[] = (($acc >> $bits) & $maxv);
168
            }
169
        }
170
171 32
        if ($pad) {
172 12
            if ($bits) {
173 12
                $ret[] = ($acc << $toBits - $bits) & $maxv;
174
            }
175 32
        } else if ($bits >= $fromBits || ((($acc << ($toBits - $bits))) & $maxv)) {
176 4
            throw new \RuntimeException("Invalid data");
177
        }
178
179 28
        return $ret;
180
    }
181
182
    /**
183
     * @param string $hrp
184
     * @param int[] $convertedDataChars
185
     * @return array
186
     */
187 12
    public static function createChecksum($hrp, array $convertedDataChars)
188
    {
189 12
        $values = array_merge(self::hrpExpand($hrp, strlen($hrp)), $convertedDataChars);
190 12
        $polyMod = self::polyMod(array_merge($values, [0, 0, 0, 0, 0, 0]), count($values) + 6) ^ 1;
191 12
        $results = [];
192 12
        for ($i = 0; $i < 6; $i++) {
193 12
            $results[$i] = ($polyMod >> 5 * (5 - $i)) & 31;
194
        }
195
196 12
        return $results;
197
    }
198
199
    /**
200
     * @param string $hrp
201
     * @param array $combinedDataChars
202
     * @return string
203
     */
204 12
    public static function encode($hrp, array $combinedDataChars)
205
    {
206 12
        $cs = self::createChecksum($hrp, $combinedDataChars);
207 12
        $combined = array_merge($combinedDataChars, $cs);
208 12
        $return = "{$hrp}1";
209 12
        for ($i = 0, $n = count($combined); $i < $n; $i++) {
210 12
            $return .= self::$CHARSET[$combined[$i]];
211
        }
212
213 12
        return $return;
214
    }
215
216
    /**
217
     * @param string $hrp
218
     * @param WitnessProgram $witnessProgram
219
     * @return string
220
     */
221 12
    public static function encodeSegwit($hrp, WitnessProgram $witnessProgram)
222
    {
223 12
        $prog = $witnessProgram->getProgram();
224 12
        $d = array_values(unpack("C*", $prog->getBinary()));
225 12
        $bits = self::convertBits($d, count($d), 8, 5);
226
227 12
        $data = array_merge(
228 12
            [$witnessProgram->getVersion()],
229
            $bits
230
        );
231
232 12
        return self::encode(
233
            $hrp,
234
            $data
235
        );
236
    }
237
238
    /**
239
     * @param string $hrp
240
     * @param string $bech32
241
     * @return WitnessProgram
242
     */
243 38
    public static function decodeSegwit($hrp, $bech32)
244
    {
245 38
        list ($hrpGot, $data) = self::decodeCheck($bech32);
246 34
        if ($hrpGot !== $hrp) {
247 14
            throw new \RuntimeException("Invalid prefix for address");
248
        }
249
250 32
        $decoded = self::convertBits(array_slice($data, 1), count($data) - 1, 5, 8, false);
251 28
        $decodeLen = count($decoded);
252 28
        if ($decodeLen < 2 || $decodeLen > 40) {
253 4
            throw new \RuntimeException("Invalid segwit address");
254
        }
255
256 24
        if ($data[0] > 16) {
257 2
            throw new \RuntimeException("Invalid witness program version");
258
        }
259
260 22
        $bytes = '';
261 22
        foreach ($decoded as $char) {
262 22
            $bytes .= chr($char);
263
        }
264 22
        $decoded = new Buffer($bytes);
265
266 22
        if (0 === $data[0]) {
267 16
            return WitnessProgram::v0($decoded);
268
        }
269
270 6
        return new WitnessProgram($data[0], $decoded);
271
    }
272
}
273