generateSeed(); [$key, $chain] = self::masterFromSeed($seed); foreach (self::parsePath($path) as $index) { [$key, $chain] = self::ckdPriv($key, $chain, $index); } $ec = new EC('secp256k1'); $pair = $ec->keyFromPrivate(bin2hex($key)); return [ 'private_key' => bin2hex($key), 'public_key_uncompressed' => $pair->getPublic(false, 'hex'), ]; } /** @return array{0: string, 1: string} binary key + chain code */ private static function masterFromSeed(string $seed): array { $I = hash_hmac('sha512', $seed, 'Bitcoin seed', true); return [substr($I, 0, 32), substr($I, 32, 32)]; } /** * @return array{0: string, 1: string} */ private static function ckdPriv(string $kPar, string $cPar, int $index): array { if ($index & 0x80000000) { $data = "\x00".$kPar.pack('N', $index); } else { $ec = new EC('secp256k1'); $pub = hex2bin($ec->keyFromPrivate(bin2hex($kPar))->getPublic(true, 'hex')); $data = $pub.pack('N', $index); } $I = hash_hmac('sha512', $data, $cPar, true); $IL = substr($I, 0, 32); $IR = substr($I, 32, 32); $n = gmp_init(self::CURVE_ORDER, 16); $ki = gmp_mod( gmp_add(gmp_init(bin2hex($IL), 16), gmp_init(bin2hex($kPar), 16)), $n ); if (gmp_cmp($ki, 0) === 0) { throw new RuntimeException('Invalid derived key'); } $key = hex2bin(str_pad(gmp_strval($ki, 16), 64, '0', STR_PAD_LEFT)); return [$key, $IR]; } /** @return list */ private static function parsePath(string $path): array { $path = trim($path); if (str_starts_with($path, 'm/')) { $path = substr($path, 2); } elseif ($path === 'm') { return []; } $out = []; foreach (explode('/', $path) as $seg) { if ($seg === '') { continue; } $hardened = str_ends_with($seg, "'") || str_ends_with($seg, 'h') || str_ends_with($seg, 'H'); $num = (int) rtrim($seg, "'hH"); if ($hardened) { $num |= 0x80000000; } $out[] = $num; } return $out; } }