✓ Copied
Monero Engine Bitcoin Engine Security Model Concurrency Anti-Enumeration
01
XMR — Monero Engine

Subaddress isolation
per invoice.

The Monero RPC daemon (monero-wallet-rpc) binds exclusively to the internal loopback interface (127.0.0.1:18084) and is never exposed to the public internet. The gateway communicates with it via authenticated cURL over localhost only.

Instead of reusing wallet addresses, the engine requests a fresh cryptographic subaddress from create_address for every single invoice. Each subaddress is mathematically derived from the master wallet but is entirely unlinkable to any other address in the set — a blockchain analyst cannot determine whether two payments went to the same wallet.

This also enables precise, per-invoice payment detection. The watcher sweeps get_transfers and matches incoming transfers against the stored invoice address directly — no subaddress index reliance, immune to schema drift.

Privacy guarantee: Even if the entire transaction history for one subaddress is publicly known, it reveals nothing about other invoices, wallet balance, or business revenue volume.
checkout_xmr.php — address generation
// RPC payload — request a fresh subaddress
$payload = json_encode([
    'jsonrpc' => '2.0',
    'id'      => '0',
    'method'  => 'create_address',
    'params'  => ['account_index' => 0],
]);

$ch = curl_init('http://127.0.0.1:18084/json_rpc');
curl_setopt_array($ch, [
    CURLOPT_POSTFIELDS      => $payload,
    CURLOPT_RETURNTRANSFER => true,
    // Loopback only — never leaves the server
]);
$res = json_decode(curl_exec($ch), true);

$address = $res['result']['address'];

// Persist address — matched by watcher, not index
$stmt = $db->prepare('INSERT INTO invoices
    (address, crypto_amount, status, expires_at)
    VALUES (?, ?, "pending", ?)');
$stmt->execute([$address, $xmrAmount, $expires]);
02
BTC — Bitcoin Engine

Watch-only architecture.
Keys never touch the server.

checkout_btc.php — zero-shell address derivation
// Build args as an array — zero shell involvement
// proc_open() bypasses OS shell entirely,
// making shell injection structurally impossible
$args = [
    BTC_ELECTRUM_CMD,
    '--offline',
    '--dir',  BTC_WALLET_DIR,
    '-w',     BTC_WALLET_FILE,
    'createnewaddress',
];

$proc = proc_open($args, [
    1 => ['pipe', 'w'],   // stdout
    2 => ['pipe', 'w'],   // stderr
], $pipes);

$addr = trim(stream_get_contents($pipes[1]));
proc_close($proc);

// Validate before storing — reject any garbage
if (!preg_match('/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,87}$/', $addr)) {
    throw new RuntimeException('Invalid BTC address');
}

Traditional gateways require a hot wallet on the server — private keys stored in plaintext or encrypted on disk. If the VPS is compromised, all funds are immediately accessible to the attacker.

The Noctyra BTC engine uses a strict watch-only Electrum wallet seeded from your master public key (zpub / xpub) only. The wallet can derive addresses and detect incoming payments but is cryptographically incapable of signing or broadcasting transactions. There are no spendable keys on the server.

Address generation uses BIP32/BIP44 hierarchical derivation, producing a fresh address for each invoice without any network request. The Electrum daemon runs in --offline mode for address generation, connecting to the network only when checking balances.

Shell injection immunity: Using proc_open(array) instead of shell_exec(string) means user-supplied data can never be interpreted as shell commands — structurally, not by sanitisation.
ApproachPrivate Keys on ServerTheft Risk if VPS CompromisedNoctyra Uses
Hot WalletYesTotal loss
Encrypted Hot WalletYes (encrypted)High — keys in memory
Watch-Only (xpub)NeverZero — cannot spend✓ This
03
Exploit Mitigation

The float-drift exploit.
Eliminated at the type level.

Most payment gateway implementations store crypto amounts as floating-point decimals — standard DOUBLE or PHP float. Due to IEEE 754 binary representation, these types cannot precisely represent many decimal fractions.

An attacker exploits this by intentionally sending an amount fractionally below the invoice total. A naive implementation using float comparison rounds up to "close enough" and marks the invoice as paid. The attacker receives their goods or credits having paid less than the required amount.

The Noctyra solution eliminates floats from all payment verification paths entirely. Every amount — at invoice creation, at RPC response, and at watcher sweep — is immediately converted to atomic integer units: Piconeros (10¹²) for XMR and Satoshis (10⁸) for BTC. Integer comparison is exact. If the attacker is short by even one single atomic unit, the strict >= check fails and the invoice remains unpaid.

Result: The attacker must pay the exact amount or more, down to 0.000000000001 XMR or 0.00000001 BTC. Rounding attacks are structurally impossible.
watcher.php — atomic integer verification
// XMR: all amounts in Piconeros (1e12 per XMR)
// BTC: all amounts in Satoshis  (1e8  per BTC)
// Floats are converted once, immediately, never used again

function to_atomic(string|int|float $v, int $multiplier): int
{
    // Handle scientific notation from RPC responses (e.g. 1.49089e+12)
    if (is_string($v) && stripos($v, 'e') !== false) {
        $v = sprintf('%.12F', (float)$v);
    }
    return (int) round((float) $v * $multiplier);
}

// Invoice amount stored in atomic units at creation
$required = (int) $invoice['crypto_amount']; // already atomic

// Amount confirmed on-chain from RPC — also atomic
$confirmed = to_atomic($transfer['amount'], XMR_ATOMIC);

// Integer comparison — exact, no rounding
if ($confirmed >= $required && $confs >= XMR_CONFIRMATIONS_REQUIRED) {
    mark_paid($invoice['id']);
}
04
Atomicity & Race Conditions

Double-webhook prevention.
Guaranteed at the database level.

watcher.php — atomic state transition
// SQLite WAL mode — enables concurrent readers
// while serialising all write transactions
$db->exec('PRAGMA journal_mode=WAL');

$db->beginTransaction();

// The WHERE clause is the entire lock.
// If status is already "paid", rowCount() = 0.
// No second process can ever fire the webhook.
$upd = $db->prepare('
    UPDATE invoices
    SET    status = "paid", paid_at = ?
    WHERE  id     = ?
    AND    status = "pending"
');
$upd->execute([time(), $invoiceId]);

$wasFirst = ($upd->rowCount() === 1);
$db->commit();

// Webhook fires once and exactly once,
// regardless of concurrent processes
if ($wasFirst) {
    trigger_noctyra_webhook($invoiceData);
}

A common failure mode in payment gateways is the double-webhook race condition. It occurs when a browser polling for payment status and the background watcher cron both process the same successful payment simultaneously — both see status as "pending", both fire the webhook, and the merchant system credits the order twice.

The Noctyra approach eliminates this by making the state transition atomic at the database level. The UPDATE ... WHERE status = "pending" pattern means only one concurrent writer can ever change the row — the database engine serialises all write transactions.

The second process that arrives — microseconds or milliseconds later — will execute the same UPDATE and receive rowCount() === 0 because the row is already "paid". The webhook never fires a second time. This guarantee holds regardless of the number of concurrent processes.

WAL mode: SQLite Write-Ahead Logging allows concurrent readers without blocking, while still serialising all writes. This keeps polling fast while maintaining write atomicity.
05
Anti-Enumeration & DDoS Defence

The gateway is
functionally invisible.

Without protection, a public checkout endpoint leaks business intelligence. An attacker iterating ?id=1, ?id=2, ... can reconstruct your entire transaction history, revenue volume, and customer cadence. Alternatively they can spam invoice creation to exhaust your database or overwhelm the wallet RPC daemon.

Every invoice URL is protected by a cryptographic view token — a per-invoice HMAC-SHA256 digest derived from the invoice ID, amount, and your private CHECKOUT_SECRET. Without the exact token, the endpoint returns a bare 404 — not even a 403. The invoice's existence is not acknowledged.

Invoice creation itself is gated behind a server-side HMAC signature on the request parameters, meaning only your merchant server with the correct CHECKOUT_SECRET can create invoices. Token comparison always uses hash_equals() — a constant-time comparison that prevents timing attacks from leaking whether a guess was close.

Rate limiting on both invoice creation and status polling is enforced per-IP within configurable time windows, backed by the SQLite database — no Redis or external dependency required.

01
Merchant Server Signs Request
HMAC-SHA256 over amount + order_id + currency using CHECKOUT_SECRET. Signature appended to checkout redirect URL.
02
Gateway Validates Signature
hash_equals() constant-time comparison. Any mismatch → immediate 403. No database query performed, no invoice created.
03
Unique View Token Issued
A second HMAC-SHA256 over the invoice ID + CHECKOUT_SECRET generates a per-invoice view token. Embedded in checkout URL. Required on every status poll.
04
Polling Rate-Limited Per IP
Status endpoint enforces configurable per-IP rate limits. Excess requests receive 429. No external cache required — enforced via SQLite WAL.
05
Webhook Signed on Delivery
Outgoing webhook payload is signed with WEBHOOK_SECRET (separate from CHECKOUT_SECRET). Merchant verifies signature before processing. Signed timestamp prevents replay attacks.

Ready to own your
infrastructure?

Stop paying 2.9% to processors who ban high-risk businesses without warning. Noctyra deploys this architecture on your VPS in a single day.

Request Deployment