diff --git a/src/BIP/HDKey.php b/src/BIP/HDKey.php index 5b1d984..5f12c8b 100644 --- a/src/BIP/HDKey.php +++ b/src/BIP/HDKey.php @@ -19,6 +19,7 @@ class HDKey 'index' => '00000000', 'privateKey' => null, 'publicKey' => null, + 'publicKeyUncompressed' => null, 'chainCode' => null, 'fingerprint' => '00000000', 'parentFingerprint' => '00000000' @@ -182,6 +183,7 @@ public function generateKeysFromPrivate(string $privateKey): void $this->data['privateKey'] = str_repeat('0', 64 - strlen($privateKey)) . $privateKey; $this->data['publicKey'] = $this->getPublicKeyFromPrivate($privateKey); + $this->data['publicKeyUncompressed'] = $this->getPublicKeyFromPrivate($privateKey, false); $this->data['fingerprint'] = $this->computeFingerprint($this->data['publicKey']); } @@ -191,7 +193,7 @@ public function generateKeysFromPrivate(string $privateKey): void * @param string $privateKey * @return string */ - protected function getPublicKeyFromPrivate(string $privateKey): string + protected function getPublicKeyFromPrivate(string $privateKey, bool $shouldCompress = true): string { $this->ellipticCurve = new EC('secp256k1'); $keyPair = new KeyPair($this->ellipticCurve, [ @@ -199,7 +201,7 @@ protected function getPublicKeyFromPrivate(string $privateKey): string 'privEnc' => 'hex' ]); - return $keyPair->getPublic(true, 'hex'); + return $keyPair->getPublic($shouldCompress, 'hex'); } /** @@ -226,12 +228,17 @@ protected function prepareDataString(bool $isHardened, string $index): string * @param $version * @return string */ - protected function encode($version) + protected function encode($version): string { + $fingerprint = intval($this->data['fingerprint']) !== 0 + ? $this->data['parentFingerprint'] + : $this->data['fingerprint']; + $fingerprintHex = Helper::hex_encode($fingerprint); + $data = [ dechex($version), Helper::hex_encode($this->data['depth']), - Helper::hex_encode(intval($this->data['fingerprint']) !== 0 ? $this->data['parentFingerprint'] : $this->data['fingerprint']), + $this->zeroLeftPad($fingerprintHex, 8), $this->convertIndexToHex($this->data['index']), $this->data['chainCode'], ($version === self::BITCOIN_VERSIONS['private'] ? $this->privateKeyWithNulls($this->data['privateKey']) : $this->data['publicKey']) @@ -282,7 +289,7 @@ protected function hmac($data, $password): array * @param string $publicKey * @return string */ - protected function computeFingerprint(string $publicKey) + protected function computeFingerprint(string $publicKey): string { $identifier = Helper::hash160($publicKey); @@ -315,6 +322,11 @@ protected function validateOptions(array $options): bool protected function convertIndexToHex(int $index): string { $indexHex = dechex($index); - return str_repeat('0', 8 - strlen($indexHex)) . $indexHex; + return $this->zeroLeftPad($indexHex, 8); + } + + protected function zeroLeftPad(string $hex, int $length): string + { + return str_pad($hex, $length, '0', STR_PAD_LEFT); } -} \ No newline at end of file +} diff --git a/tests/BIP32Test.php b/tests/BIP32Test.php new file mode 100644 index 0000000..3e3ba2d --- /dev/null +++ b/tests/BIP32Test.php @@ -0,0 +1,155 @@ + '000102030405060708090a0b0c0d0e0f', + 'cases' => [ + [ + 'path' => "m", + 'xpub' => 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', + 'xprv' => 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi', + ], + [ + 'path' => "m/0'", + 'xpub' => 'xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw', + 'xprv' => 'xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7', + ], + [ + 'path' => "m/0'/1", + 'xpub' => 'xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ', + 'xprv' => 'xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs', + ], + [ + 'path' => "m/0'/1/2'", + 'xpub' => 'xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5', + 'xprv' => 'xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM', + ], + [ + 'path' => "m/0'/1/2'/2", + 'xpub' => 'xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV', + 'xprv' => 'xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334', + ], + [ + 'path' => "m/0'/1/2'/2/1000000000", + 'xpub' => 'xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy', + 'xprv' => 'xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76', + ], + ] + ]; + + const TEST_VECTOR2 = [ + 'seed' => 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542', + 'cases' => [ + [ + 'path' => "m", + 'xpub' => 'xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB', + 'xprv' => 'xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U', + ], + [ + 'path' => "m/0", + 'xpub' => 'xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH', + 'xprv' => 'xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt', + ], + [ + 'path' => "m/0/2147483647'", + 'xpub' => 'xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a', + 'xprv' => 'xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9', + ], + [ + 'path' => "m/0/2147483647'/1", + 'xpub' => 'xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon', + 'xprv' => 'xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef', + ], + [ + 'path' => "m/0/2147483647'/1/2147483646'", + 'xpub' => 'xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL', + 'xprv' => 'xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc', + ], + [ + 'path' => "m/0/2147483647'/1/2147483646'/2", + 'xpub' => 'xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt', + 'xprv' => 'xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j', + ], + ], + ]; + + const TEST_VECTOR3 = [ + 'seed' => '4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be', + 'cases' => [ + [ + 'path' => "m", + 'xpub' => 'xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13', + 'xprv' => 'xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6', + ], + [ + 'path' => "m/0'", + 'xpub' => 'xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y', + 'xprv' => 'xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L', + ], + ], + ]; + + const TEST_VECTOR4 = [ + 'seed' => '3ddd5602285899a946114506157c7997e5444528f3003f6134712147db19b678', + 'cases' => [ + [ + 'path' => "m", + 'xpub' => 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa', + 'xprv' => 'xprv9s21ZrQH143K48vGoLGRPxgo2JNkJ3J3fqkirQC2zVdk5Dgd5w14S7fRDyHH4dWNHUgkvsvNDCkvAwcSHNAQwhwgNMgZhLtQC63zxwhQmRv', + ], + [ + 'path' => "m/0'", + 'xpub' => 'xpub69AUMk3qDBi3uW1sXgjCmVjJ2G6WQoYSnNHyzkmdCHEhSZ4tBok37xfFEqHd2AddP56Tqp4o56AePAgCjYdvpW2PU2jbUPFKsav5ut6Ch1m', + 'xprv' => 'xprv9vB7xEWwNp9kh1wQRfCCQMnZUEG21LpbR9NPCNN1dwhiZkjjeGRnaALmPXCX7SgjFTiCTT6bXes17boXtjq3xLpcDjzEuGLQBM5ohqkao9G', + ], + [ + 'path' => "m/0'/1'", + 'xpub' => 'xpub6BJA1jSqiukeaesWfxe6sNK9CCGaujFFSJLomWHprUL9DePQ4JDkM5d88n49sMGJxrhpjazuXYWdMf17C9T5XnxkopaeS7jGk1GyyVziaMt', + 'xprv' => 'xprv9xJocDuwtYCMNAo3Zw76WENQeAS6WGXQ55RCy7tDJ8oALr4FWkuVoHJeHVAcAqiZLE7Je3vZJHxspZdFHfnBEjHqU5hG1Jaj32dVoS6XLT1', + ], + ], + ]; + + public function testVector1() + { + $this->runTestVector(self::TEST_VECTOR1); + } + + public function testVector2() + { + $this->runTestVector(self::TEST_VECTOR2); + } + + public function testVector3() + { + $this->runTestVector(self::TEST_VECTOR3); + } + + public function testVector4() + { + $this->runTestVector(self::TEST_VECTOR4); + } + + protected function runTestVector(array $vector) + { + foreach($vector['cases'] as $case) + { + $HDKey = BIP44::fromMasterSeed($vector['seed'])->derive($case['path']); + $this->assertEquals($case['xpub'], $HDKey->getPublicExtendedKey()); + $this->assertEquals($case['xprv'], $HDKey->getPrivateExtendedKey()); + } + } +}