Skip to content

Commit 2e58d03

Browse files
committed
Initial commit
0 parents  commit 2e58d03

File tree

7 files changed

+729
-0
lines changed

7 files changed

+729
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vendor

.travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: php
2+
php:
3+
- 7.1
4+
5+
cache:
6+
directories:
7+
- vendor
8+
9+
install:
10+
- composer install
11+
12+
script:
13+
- php tests.php

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Nimiq XPub
2+
3+
A simple class to derive BTC and ETH addresses without GMP.

XPub.php

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
require_once('vendor/autoload.php');
4+
5+
use StephenHill\Base58;
6+
use Elliptic\EC;
7+
use BN\BN;
8+
use kornrunner\Keccak;
9+
use BitWasp\Bech32;
10+
11+
class XPub {
12+
public const HEX_VERSION = [
13+
'xpub' => '0488b21e',
14+
'tpub' => '043587cf',
15+
'zpub' => '04b24746',
16+
'vpub' => '045f1cf6',
17+
];
18+
19+
// https://en.bitcoin.it/wiki/List_of_address_prefixes
20+
public const NETWORK_ID = [
21+
'xpub' => '00',
22+
'tpub' => '6f',
23+
];
24+
25+
public const SEGWIT_HRP = [
26+
'zpub' => 'bc',
27+
'vpub' => 'tc',
28+
];
29+
30+
public const SEGWIT_VERSION = 0;
31+
32+
public static function fromString(string $xpub_base58): XPub {
33+
$xpub_bin = (new Base58())->decode($xpub_base58);
34+
35+
$version = substr($xpub_base58, 0, 4);
36+
$depth = self::bin2dec(substr($xpub_bin, 4, 1));
37+
$fpr_par = bin2hex(substr($xpub_bin, 5, 4));
38+
$i = self::bin2dec(substr($xpub_bin, 9, 4));
39+
$c = bin2hex(substr($xpub_bin, 13, 32));
40+
$K = bin2hex(substr($xpub_bin, 45, 33));
41+
42+
return new self(
43+
$version,
44+
$depth,
45+
$fpr_par,
46+
$i,
47+
$c,
48+
$K
49+
);
50+
}
51+
52+
public static function bin2dec(string $bin): int {
53+
return unpack('C', $bin)[1];
54+
}
55+
56+
public static function hash160(string $hex): string {
57+
return hash('ripemd160', hash('sha256', hex2bin($hex), TRUE));
58+
}
59+
60+
public static function doubleSha256(string $hex): string {
61+
return hash('sha256', hash('sha256', hex2bin($hex), TRUE));
62+
}
63+
64+
public function __construct(
65+
string $version,
66+
int $depth,
67+
string $parent_fingerprint,
68+
int $index,
69+
string $c,
70+
string $K
71+
) {
72+
$this->version = $version;
73+
$this->depth = $depth;
74+
$this->parent_fingerprint = $parent_fingerprint;
75+
$this->index = $index;
76+
$this->c = $c;
77+
$this->K = $K;
78+
}
79+
80+
public function derive($indices): XPub {
81+
if (!is_array($indices)) $indices = [$indices];
82+
$i = array_shift($indices);
83+
84+
$ec = new EC('secp256k1'); // BTC Elliptic Curve
85+
86+
$I_key = hex2bin($this->c);
87+
$I_data = hex2bin($this->K) . pack('N', $i);
88+
$I = hash_hmac('sha512', $I_data, $I_key);
89+
$I_L = substr($I, 0, 64);
90+
$I_R = substr($I, 64, 64);
91+
$c_i = $I_R; // Child Chain Code
92+
93+
$K_par_point = $ec->curve->decodePoint($this->K, 'hex');
94+
$I_L_point = $ec->g->mul(new BN($I_L, 16));
95+
$K_i = $K_par_point->add($I_L_point);
96+
$K_i = $K_i->encodeCompressed('hex'); // Child Public Key
97+
98+
$fpr_par = substr(self::hash160($this->K), 0, 8); // Parent Fingerprint
99+
100+
$child = new self(
101+
$this->version,
102+
$this->depth + 1,
103+
$fpr_par,
104+
$i,
105+
$c_i,
106+
$K_i
107+
);
108+
109+
// Recursive derivation
110+
if (count($indices) > 0) return $child->derive($indices);
111+
112+
return $child;
113+
}
114+
115+
public function toString(bool $asHex = false): string {
116+
$xpub_hex = self::HEX_VERSION[$this->version];
117+
$xpub_hex .= str_pad(dechex($this->depth), 2, '0', STR_PAD_LEFT);
118+
$xpub_hex .= $this->parent_fingerprint;
119+
$xpub_hex .= str_pad(dechex($this->index), 8, '0', STR_PAD_LEFT);
120+
$xpub_hex .= $this->c;
121+
$xpub_hex .= $this->K;
122+
123+
// Checksum
124+
$xpub_hex .= substr(self::doubleSha256($xpub_hex), 0, 8);
125+
126+
if ($asHex) return $xpub_hex;
127+
128+
return (new Base58())->encode(hex2bin($xpub_hex));
129+
}
130+
131+
public function toAddress(string $coin = 'btc') {
132+
switch ($coin) {
133+
case 'btc': return $this->toBTCAddress();
134+
case 'eth': return $this->toETHAddress();
135+
default: throw new Exception('Coin type "' . $coin . '" not supported!');
136+
}
137+
}
138+
139+
private function toBTCAddress(): string {
140+
switch ($this->version) {
141+
case 'xpub': case 'tpub': return $this->toBTCP2PKHAddress();
142+
case 'zpub': case 'vpub': return $this->toBTCP2WPKHAddress();
143+
default: throw new Exception('Version "' . $this->version . '" not supported!');
144+
}
145+
}
146+
147+
private function toBTCP2PKHAddress(): string {
148+
$base_address = self::NETWORK_ID[$this->version] . self::hash160($this->K);
149+
$checksum = substr(self::doubleSha256($base_address), 0, 8);
150+
151+
$address_hex = $base_address . $checksum;
152+
153+
return (new Base58())->encode(hex2bin($address_hex));
154+
}
155+
156+
private function toBTCP2WPKHAddress(): string {
157+
$programm = self::hash160($this->K);
158+
$version = self::SEGWIT_VERSION;
159+
$hrp = self::SEGWIT_HRP[$this->version];
160+
return Bech32\encodeSegwit($hrp, $version, hex2bin($programm));
161+
}
162+
163+
private function toETHAddress(): string {
164+
$ec = new EC('secp256k1'); // ETH Elliptic Curve
165+
$K_full = $ec->keyFromPublic($this->K, 'hex')->getPublic('hex');
166+
167+
$K_bin = hex2bin(substr($K_full, 2));
168+
$hash_hex = Keccak::hash($K_bin, 256);
169+
$base_address = substr($hash_hex, 24, 40);
170+
171+
return '0x' . $this->encodeETHChecksum($base_address);
172+
}
173+
174+
private function encodeETHChecksum(string $base_address) {
175+
$binary = $this->hex2binary(Keccak::hash($base_address, 256));
176+
177+
$encoded = '';
178+
foreach (str_split($base_address) as $i => $char) {
179+
if (strpos('abcdef', $char) !== false) {
180+
$encoded .= $binary[$i * 4] === '1' ? strtoupper($char) : strtolower($char);
181+
} else {
182+
$encoded .= $char;
183+
}
184+
}
185+
return $encoded;
186+
}
187+
188+
private function hex2binary($hex) {
189+
$binary = '';
190+
foreach (str_split($hex, 2) as $hexit) {
191+
$binary .= str_pad(decbin(hexdec($hexit)), 8, '0', STR_PAD_LEFT);
192+
}
193+
return $binary;
194+
}
195+
}

composer.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "nimiq/xpub",
3+
"description": "A simple class to derive BTC and ETH addresses without GMP.",
4+
"type": "library",
5+
"license": "Apache-2.0",
6+
"authors": [
7+
{
8+
"name": "Sören Schwert",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"minimum-stability": "stable",
13+
"require": {
14+
"simplito/elliptic-php": "^1.0",
15+
"stephenhill/base58": "^1.1",
16+
"kornrunner/keccak": "^1.0",
17+
"bitwasp/bech32": "^0.0.1"
18+
}
19+
}

0 commit comments

Comments
 (0)