Skip to content

Add bech32 validation for BTC and LTC #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions src/Utils/Bech32Decoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Merkeleon\PhpCryptocurrencyAddressValidation\Utils;

/**
* @see https://github.com/Bit-Wasp/bech32/blob/master/src/bech32.php
*/
class Bech32Decoder
{
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
const CHARKEY_KEY = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1,
-1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1,
1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1
];

/**
* @throws Bech32Exception
* @param string $sBech - the bech32 encoded string
* @return array - returns [$hrp, $dataChars]
*/
static public function decodeRaw($sBech)
{
$length = \strlen($sBech);
if ($length < 8) {
throw new Bech32Exception("Bech32 string is too short");
}
$chars = array_values(unpack('C*', $sBech));
$haveUpper = false;
$haveLower = false;
$positionOne = -1;
for ($i = 0; $i < $length; $i++) {
$x = $chars[$i];
if ($x < 33 || $x > 126) {
throw new Bech32Exception('Out of range character in bech32 string');
}
if ($x >= 0x61 && $x <= 0x7a) {
$haveLower = true;
}
if ($x >= 0x41 && $x <= 0x5a) {
$haveUpper = true;
$x = $chars[$i] = $x + 0x20;
}
// find location of last '1' character
if ($x === 0x31) {
$positionOne = $i;
}
}
if ($haveUpper && $haveLower) {
throw new Bech32Exception('Data contains mixture of higher/lower case characters');
}
if ($positionOne === -1) {
throw new Bech32Exception("Missing separator character");
}
if ($positionOne < 1) {
throw new Bech32Exception("Empty HRP");
}
if (($positionOne + 7) > $length) {
throw new Bech32Exception('Too short checksum');
}
$hrp = \pack("C*", ...\array_slice($chars, 0, $positionOne));
$data = [];
for ($i = $positionOne + 1; $i < $length; $i++) {
$data[] = ($chars[$i] & 0x80) ? -1 : self::CHARKEY_KEY[$chars[$i]];
}
if (!self::verifyChecksum($hrp, $data)) {
throw new Bech32Exception('Invalid bech32 checksum');
}

return [$hrp, array_slice($data, 0, -6)];
}

/**
* Verifies the checksum given $hrp and $convertedDataChars.
*
* @param string $hrp
* @param int[] $convertedDataChars
* @return bool
*/
private static function verifyChecksum($hrp, array $convertedDataChars)
{
$expandHrp = self::hrpExpand($hrp, \strlen($hrp));
$r = \array_merge($expandHrp, $convertedDataChars);
$poly = self::polyMod($r, \count($r));
return $poly === 1;
}

/**
* Expands the human readable part into a character array for checksumming.
* @param string $hrp
* @param int $hrpLen
* @return int[]
*/
private static function hrpExpand($hrp, $hrpLen)
{
$expand1 = [];
$expand2 = [];
for ($i = 0; $i < $hrpLen; $i++) {
$o = \ord($hrp[$i]);
$expand1[] = $o >> 5;
$expand2[] = $o & 31;
}
return \array_merge($expand1, [0], $expand2);
}

/**
* @param int[] $values
* @param int $numValues
* @return int
*/
private static function polyMod(array $values, $numValues)
{
$chk = 1;
for ($i = 0; $i < $numValues; $i++) {
$top = $chk >> 25;
$chk = ($chk & 0x1ffffff) << 5 ^ $values[$i];
for ($j = 0; $j < 5; $j++) {
$value = (($top >> $j) & 1) ? self::GENERATOR[$j] : 0;
$chk ^= $value;
}
}
return $chk;
}
}
8 changes: 8 additions & 0 deletions src/Utils/Bech32Exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Merkeleon\PhpCryptocurrencyAddressValidation\Utils;

class Bech32Exception extends \Exception
{

}
16 changes: 16 additions & 0 deletions src/Validation/BTC.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation;

use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Decoder;
use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Exception;
use Merkeleon\PhpCryptocurrencyAddressValidation\Validation;

class BTC extends Validation
Expand All @@ -11,4 +13,18 @@ class BTC extends Validation
'1' => '00',
'3' => '05'
];

public function validate($address)
{
$valid = parent::validate($address);

if (!$valid) {
// maybe it's a bech32 address
try {
$valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'bc' === $decoded[0];
} catch (Bech32Exception $exception) {}
}

return $valid;
}
}
16 changes: 16 additions & 0 deletions src/Validation/LTC.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Merkeleon\PhpCryptocurrencyAddressValidation\Validation;

use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Decoder;
use Merkeleon\PhpCryptocurrencyAddressValidation\Utils\Bech32Exception;
use Merkeleon\PhpCryptocurrencyAddressValidation\Validation;

class LTC extends Validation
Expand All @@ -17,6 +19,20 @@ class LTC extends Validation
'3' => '05'
];

public function validate($address)
{
$valid = parent::validate($address);

if (!$valid) {
// maybe it's a bech32 address
try {
$valid = is_array($decoded = Bech32Decoder::decodeRaw($address)) && 'ltc' === $decoded[0];
} catch (Bech32Exception $exception) {}
}

return $valid;
}

protected function validateVersion($version)
{
if (!$this->deprecatedAllowed && in_array($this->addressVersion, self::DEPRECATED_ADDRESS_VERSIONS)) {
Expand Down
4 changes: 3 additions & 1 deletion tests/BTCTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public function testValidator()
$testData = [
['1QLbGuc3WGKKKpLs4pBp9H6jiQ2MgPkXRp', true],
['3QJmV3qfvL9SuYo34YihAf3sRCW3qSinyC', true],
['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', false]
['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', false],
['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', true],
['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', false]
];

foreach ($testData as $row) {
Expand Down
2 changes: 2 additions & 0 deletions tests/LTCTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public function testValidator()
['3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj', true],
['LbTjMGN7gELw4KbeyQf6cTCq859hD18guE', true],
['MJRSgZ3UUFcTBTBAaN38XAXvZLwRe8WVw7', true],
['ltc1qy4rwhdkujk35ga26774gqmng67kgggtqnsx9vp0xgzp3wz3yjkhqashszw', true],
['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq', false]
];

foreach ($testData as $row) {
Expand Down