<?php
// pubkey_to_xy_form.php
// Convert a secp256k1 public key (compressed/uncompressed) to (X,Y) coordinates.
// Accepts hex with or without 0x. Requires: PHP GMP extension.

function h($s){ return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); }

// secp256k1 params
const P_HEX = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'; // field prime p
const A_HEX = '0000000000000000000000000000000000000000000000000000000000000000'; // a = 0
const B_HEX = '0000000000000000000000000000000000000000000000000000000000000007'; // b = 7

$p = gmp_init(P_HEX, 16);
$a = gmp_init(A_HEX, 16);
$b = gmp_init(B_HEX, 16);

function clean_hex($hex): string {
    $hex = trim($hex);
    if (str_starts_with(strtolower($hex), '0x')) $hex = substr($hex, 2);
    $hex = preg_replace('/\s+/', '', $hex);
    if ($hex === '') throw new Exception("Empty input.");
    if (!preg_match('/^[0-9a-fA-F]+$/', $hex)) throw new Exception("Invalid hex string.");
    if (strlen($hex) % 2 === 1) $hex = '0' . $hex; // even length
    return strtolower($hex);
}

function gmp_from_hex($hex): GMP { return gmp_init($hex, 16); }
function hex_from_gmp64(GMP $n): string {
    $h = gmp_strval($n, 16);
    return strtoupper(str_pad($h, 64, '0', STR_PAD_LEFT));
}
function is_even(GMP $n): bool { return gmp_intval(gmp_mod($n, 2)) === 0; }

function mod_add(GMP $x, GMP $y, GMP $p): GMP { return gmp_mod(gmp_add($x, $y), $p); }
function mod_sub(GMP $x, GMP $y, GMP $p): GMP { return gmp_mod(gmp_sub($x, $y), $p); }
function mod_mul(GMP $x, GMP $y, GMP $p): GMP { return gmp_mod(gmp_mul($x, $y), $p); }
function mod_pow(GMP $x, GMP $e, GMP $p): GMP { return gmp_powm($x, $e, $p); }

function mod_sqrt_secp256k1(GMP $n, GMP $p): ?GMP {
    // p % 4 == 3 → sqrt = n^((p+1)/4) mod p  (if quadratic residue)
    if (gmp_cmp($n, 0) == 0) return gmp_init(0);
    $exp = gmp_div_q(gmp_add($p, 1), 4);
    $y = gmp_powm($n, $exp, $p);
    // verify: y^2 == n (mod p)
    if (gmp_cmp(mod_mul($y, $y, $p), gmp_mod($n, $p)) === 0) return $y;
    return null; // not a residue
}

function is_on_curve(GMP $x, GMP $y, GMP $a, GMP $b, GMP $p): bool {
    // y^2 == x^3 + a*x + b (mod p)
    $lhs = mod_mul($y, $y, $p);
    $x2  = mod_mul($x, $x, $p);
    $x3  = mod_mul($x2, $x, $p);
    $rhs = mod_add(mod_add($x3, mod_mul($a, $x, $p), $p), $b, $p);
    return gmp_cmp($lhs, $rhs) === 0;
}

function parse_pubkey_to_xy(string $pub_hex, GMP $a, GMP $b, GMP $p): array {
    $hex = clean_hex($pub_hex);
    $len = strlen($hex);

    if ($len === 130 && str_starts_with($hex, '04')) {
        // Uncompressed: 04 || X(32B) || Y(32B)
        $x_hex = substr($hex, 2, 64);
        $y_hex = substr($hex, 66, 64);
        $x = gmp_from_hex($x_hex);
        $y = gmp_from_hex($y_hex);
        if (!is_on_curve($x, $y, $a, $b, $p)) {
            throw new Exception("Point not on curve (invalid uncompressed pubkey).");
        }
        return [
            'type' => 'uncompressed',
            'x' => $x, 'y' => $y,
            'prefix' => '04',
        ];
    }

    if ($len === 66 && (str_starts_with($hex, '02') || str_starts_with($hex, '03'))) {
        // Compressed: 02/03 || X(32B)
        $prefix = substr($hex, 0, 2);
        $x_hex  = substr($hex, 2, 64);
        $x = gmp_from_hex($x_hex);

        // Compute y from y^2 = x^3 + 7 mod p  (a=0, b=7)
        global $b, $p;
        $x2  = mod_mul($x, $x, $p);
        $x3  = mod_mul($x2, $x, $p);
        $rhs = mod_add($x3, $b, $p);
        $y   = mod_sqrt_secp256k1($rhs, $p);
        if ($y === null) throw new Exception("No modular square root; invalid compressed pubkey.");

        // Two roots: y and (p - y). Choose parity to match prefix.
        $y_alt = gmp_sub($p, $y);
        $want_even = ($prefix === '02');
        $y_final = (is_even($y) === $want_even) ? $y : $y_alt;

        if (!is_on_curve($x, $y_final, $a, $b, $p)) {
            throw new Exception("Recovered point not on curve (should not happen).");
        }

        return [
            'type' => 'compressed',
            'x' => $x, 'y' => $y_final,
            'prefix' => $prefix,
        ];
    }

    // Try to be helpful with common mistakes
    if ($len === 128) {
        throw new Exception("Looks like missing 0x04 prefix for uncompressed key (should be 65 bytes / 130 hex incl. 04).");
    }
    if ($len === 64) {
        throw new Exception("Only X provided. Compressed keys must be 33 bytes (66 hex) starting with 02/03.");
    }

    throw new Exception("Unsupported pubkey format/length. Expected 33-byte compressed (02/03 + 32B X) or 65-byte uncompressed (04 + 32B X + 32B Y).");
}

// ---------- Web UI ----------
$sample_uncompressed = '04'
    .'79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798'
    .'483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8'; // G point (secp256k1)

$sample_compressed = '02'
    .'79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798';

$result = null;
$error  = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $in = $_POST['pubkey'] ?? '';
    try {
        if (!extension_loaded('gmp')) {
            throw new Exception("PHP GMP extension is not enabled. Please enable/install GMP.");
        }
        $parsed = parse_pubkey_to_xy($in, $a, $b, $p);
        $x = $parsed['x']; $y = $parsed['y'];

        $result = [
            'type'        => $parsed['type'],
            'prefix'      => $parsed['prefix'],
            'x_hex'       => hex_from_gmp64($x),
            'y_hex'       => hex_from_gmp64($y),
            'x_dec'       => gmp_strval($x, 10),
            'y_dec'       => gmp_strval($y, 10),
            'y_parity'    => is_even($y) ? 'even' : 'odd',
            'recompressed'=> (is_even($y) ? '02' : '03') . hex_from_gmp64($x),
            'reuncompressed'=> '04' . hex_from_gmp64($x) . hex_from_gmp64($y),
            'on_curve'    => 'true',
        ];
    } catch (Throwable $e) {
        $error = $e->getMessage();
    }
}
?>
<!doctype html>
<html lang="fa" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Public Key → (X,Y) | secp256k1</title>
<style>
  :root { --bg:#0f172a; --card:#111827; --fg:#e5e7eb; --muted:#9ca3af; --acc:#38bdf8; }
  *{box-sizing:border-box}
  body{margin:0; font-family:ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto; background:var(--bg); color:var(--fg);}
  .wrap{max-width:980px; margin:40px auto; padding:16px;}
  .card{background:var(--card); border:1px solid #1f2937; border-radius:16px; padding:24px; box-shadow:0 10px 30px rgba(0,0,0,.25);}
  h1{margin:0 0 8px; font-size:24px;}
  p.sub{color:var(--muted); margin-top:0}
  label{display:block; margin-top:16px; font-weight:600;}
  textarea{
    width:100%; min-height:110px; padding:12px 14px; border-radius:12px; border:1px solid #334155;
    background:#0b1220; color:var(--fg); font-family:monospace; font-size:14px;
  }
  .btn{margin-top:18px; background:var(--acc); color:#041318; font-weight:800; border:none; padding:12px 16px; border-radius:12px; cursor:pointer;}
  .btn:hover{filter:brightness(1.1)}
  .out{margin-top:22px; padding:16px; background:#0b1220; border:1px dashed #334155; border-radius:12px; font-family:monospace; overflow:auto}
  .kvs{display:grid; grid-template-columns:220px 1fr; gap:8px; align-items:center}
  .kv{display:contents}
  .err{background:#3b0d0d; color:#fecaca; border:1px solid #7f1d1d; padding:12px 14px; border-radius:12px; margin-top:14px;}
  .muted{color:var(--muted)}
  .row{display:grid; grid-template-columns:1fr; gap:16px;}
  code{background:#0b1220; padding:2px 6px; border-radius:6px; border:1px solid #334155}
</style>
</head>
<body>
  <div class="wrap">
    <div class="card">
      <h1>تبدیل Public Key (Compressed/Uncompressed) به مختصات X,Y</h1>
      <p class="sub">کلید عمومی را به صورت هگز وارد کنید. مثال: <code><?=h($sample_compressed)?></code> یا <code><?=h($sample_uncompressed)?></code></p>

      <form method="post" action="">
        <label for="pubkey">Public Key (Hex)</label>
        <textarea id="pubkey" name="pubkey" placeholder="02/03 + X (برای فشرده) یا 04 + X + Y (برای غیر فشرده)"><?=h($_POST['pubkey'] ?? $sample_compressed)?></textarea>
        <button class="btn" type="submit">استخراج مختصات</button>
      </form>

      <?php if ($error): ?>
        <div class="err">خطا: <?=h($error)?></div>
      <?php endif; ?>

      <?php if ($result && !$error): ?>
        <div class="out">
          <div class="kvs">
            <div class="kv"><strong>Type:</strong> <div><?=h($result['type'])?></div></div>
            <div class="kv"><strong>Prefix:</strong> <div><?=h($result['prefix'])?></div></div>
            <div class="kv"><strong>X (hex 64b):</strong> <div><?=h($result['x_hex'])?></div></div>
            <div class="kv"><strong>Y (hex 64b):</strong> <div><?=h($result['y_hex'])?></div></div>
            <div class="kv"><strong>X (decimal):</strong> <div style="white-space:pre-wrap"><?=h($result['x_dec'])?></div></div>
            <div class="kv"><strong>Y (decimal):</strong> <div style="white-space:pre-wrap"><?=h($result['y_dec'])?></div></div>
            <div class="kv"><strong>Y parity:</strong> <div><?=h($result['y_parity'])?></div></div>
            <div class="kv"><strong>Recompressed:</strong> <div><?=h($result['recompressed'])?></div></div>
            <div class="kv"><strong>Re-uncompressed:</strong> <div><?=h($result['reuncompressed'])?></div></div>
            <div class="kv"><strong>On curve:</strong> <div><?=h($result['on_curve'])?></div></div>
          </div>
        </div>
        <p class="muted">اعتبارسنجی: «Recompressed» باید با نوع و X ورودی هم‌خوانی داشته باشد؛ و نقطه باید روی منحنی باشد.</p>
      <?php endif; ?>
    </div>
  </div>
</body>
</html>
