Sistem sewa komputasi GPU on-demand membutuhkan latensi provisioning rendah dan settlement pembayaran yang tepat. Ketika mengintegrasikan blockchain Solana sebagai rel pembayaran (seperti pada arsitektur OpenRAM atau protokol dePIN lainnya), backend layanan komputasi umumnya menerima event transaksi melalui webhook streaming seperti Helius, QuickNode, atau custom WebSocket poller. Masalah kritis muncul ketika provider webhook mengirim event berulang kali (at-least-once delivery) atau transaksi Solana mengalami network fork sementara backend menerima lonjakan panggilan serentak.

Jika tidak ditangani secara idempotensial, sistem rentan terhadap double-credit vulnerability—di mana satu transfer token atau SOL dialokasikan menjadi durasi sewa GPU dua kali lipat. Artikel ini menyajikan pola arsitektur deduplikasi webhook Solana, verifikasi cryptographic signature on-chain, mitigasi race condition via PostgreSQL atomic lock, serta perancangan dead-letter queue (DLQ) untuk payload bermasalah.

Vektor Masalah: Mengapa Idempotensi Webhook Solana Kompleks?

Penanganan webhook pembayaran Web3 memiliki karakteristik kegagalan yang berbeda dibandingkan payment gateway tradisional (Stripe atau Xendit):

  • Retry Storm: Saat provider webhook mengalami timeout jaringan atau menerima HTTP 5xx dari server backend, provider akan melakukan exponential backoff retry. Jika database backend sedang mengalami saturasi koneksi, tumpukan retry ini dapat melumpuhkan service container.
  • Status Komitmen Solana: Payload webhook dapat dipancarkan pada status komitmen processed, confirmed, atau finalized. Memproses alokasi GPU sebelum transaksi mencapai finalized berisiko terkena micro-fork rollback, menyebabkan node GPU dialokasikan tanpa pembayaran riil.
  • Race Condition Double-Credit: Pengiriman webhook secara paralel dari dua instance bridge dapat mengeksekusi dua query SELECT balance bersamaan sebelum salah satunya mencatat record ledger (read-modify-write hazard).

Arsitektur Deduplikasi dan Pipeline Settlement

Untuk memastikan alokasi cluster GPU bersifat exactly-once processing, alur pemrosesan webhook dipisahkan menjadi dua layer: ingestion API yang stateless dan worker queue asinkron yang terisolasi dengan isolasi transaksi ACID.

Webhook Ingress (Fastify / Express)
  │
  ├── 1. Validasi Auth Header / HMAC Secret
  ├── 2. Ekstraksi Solana Transaction Signature
  ├── 3. Redis SETNX (Distributed Lock TTL 30s)
  │      └── Lock Gagal? Return HTTP 200 OK (Mencegah retry storm)
  │
  └── 4. Push Job ke Message Queue (BullMQ / RabbitMQ)
         │
         ▼
Worker Settlement Process
  │
  ├── 5. Query On-Chain Solana RPC (Verifikasi mint, receiver, amount, finality)
  ├── 6. DB Transaction (PostgreSQL):
  │      ├── INSERT INTO webhook_events (tx_signature, status) VALUES (...) ON CONFLICT DO NOTHING;
  │      └── UPDATE gpu_instances SET rental_expiry = rental_expiry + :allocated_time WHERE id = :instance_id;
  │
  └── 7. Error / Unconfirmed Payload -> Move to Dead-Letter Queue (DLQ)

Implementasi Teknis: TypeScript Handler dengan Atomic Lock

Contoh berikut mendemonstrasikan implementasi handler settlement GPU menggunakan TypeScript, @solana/web3.js, dan PostgreSQL client dengan native locking.

1. Skema Database Ledger

CREATE TABLE gpu_rental_ledger (
    id BIGSERIAL PRIMARY KEY,
    tx_signature VARCHAR(88) NOT NULL UNIQUE,
    user_wallet VARCHAR(44) NOT NULL,
    gpu_instance_id UUID NOT NULL,
    amount_lamports BIGINT NOT NULL,
    rent_seconds_added INTEGER NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PROCESSED',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE TABLE gpu_instances (
    id UUID PRIMARY KEY,
    status VARCHAR(20) NOT NULL,
    current_expiry TIMESTAMP WITH TIME ZONE NOT NULL
);

2. Idempotent Settlement Worker

import { Connection, PublicKey, ParsedTransactionWithMeta } from '@solana/web3.js';
import { Pool, PoolClient } from 'pg';

interface WebhookJobPayload {
  signature: string;
  gpuInstanceId: string;
  expectedLamports: number;
  treasuryWallet: string;
}

const SECONDS_PER_SOL_RATE = 3600; // Contoh: 1 SOL = 1 jam GPU A100

export class GpuSettlementService {
  constructor(
    private readonly dbPool: Pool,
    private readonly solanaConnection: Connection
  ) {}

  public async processWebhook(payload: WebhookJobPayload): Promise<void> {
    const { signature, gpuInstanceId, expectedLamports, treasuryWallet } = payload;

    // 1. Verifikasi On-Chain State langsung ke Node Solana (Bypass payload trust)
    const tx: ParsedTransactionWithMeta | null = await this.solanaConnection.getParsedTransaction(
      signature,
      {
        commitment: 'finalized',
        maxSupportedTransactionVersion: 0,
      }
    );

    if (!tx || tx.meta?.err) {
      throw new Error(`Transaksi ${signature} tidak valid atau dibatalkan on-chain`);
    }

    // 2. Verifikasi transfer token / lamport ke treasury target
    const isPaymentValid = this.verifyPaymentTransfer(tx, treasuryWallet, expectedLamports);
    if (!isPaymentValid) {
      throw new Error(`Payload mismatch: transfer tidak memenuhi amount atau receiver target`);
    }

    const payerWallet = tx.transaction.message.accountKeys[0].pubkey.toBase58();
    const allocatedSeconds = Math.floor((expectedLamports / 1e9) * SECONDS_PER_SOL_RATE);

    // 3. Eksekusi Atomic Lock di PostgreSQL
    const client: PoolClient = await this.dbPool.connect();
    try {
      await client.query('BEGIN');

      // Unique constraint check via INSERT ... ON CONFLICT
      const ledgerInsertResult = await client.query(
        `INSERT INTO gpu_rental_ledger 
          (tx_signature, user_wallet, gpu_instance_id, amount_lamports, rent_seconds_added, status)
         VALUES ($1, $2, $3, $4, $5, 'CONFIRMED')
         ON CONFLICT (tx_signature) DO NOTHING
         RETURNING id`,
        [signature, payerWallet, gpuInstanceId, expectedLamports, allocatedSeconds]
      );

      // Jika baris tidak ter-insert, signature sudah pernah diproses (Idempotency Triggered)
      if (ledgerInsertResult.rowCount === 0) {
        await client.query('ROLLBACK');
        return;
      }

      // Lock row instance GPU menggunakan SELECT ... FOR UPDATE untuk isolasi deterministik
      const instanceRes = await client.query(
        `SELECT current_expiry FROM gpu_instances WHERE id = $1 FOR UPDATE`,
        [gpuInstanceId]
      );

      if (instanceRes.rowCount === 0) {
        throw new Error(`GPU Instance ${gpuInstanceId} tidak ditemukan`);
      }

      const currentExpiry = new Date(instanceRes.rows[0].current_expiry);
      const baseTime = currentExpiry > new Date() ? currentExpiry : new Date();
      const newExpiry = new Date(baseTime.getTime() + allocatedSeconds * 1000);

      await client.query(
        `UPDATE gpu_instances 
         SET current_expiry = $1, status = 'ACTIVE' 
         WHERE id = $2`,
        [newExpiry, gpuInstanceId]
      );

      await client.query('COMMIT');
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }

  private verifyPaymentTransfer(
    tx: ParsedTransactionWithMeta,
    expectedReceiver: string,
    expectedLamports: number
  ): boolean {
    // Inspeksi accountKeys dan transfer instruction balance diff
    const accounts = tx.transaction.message.accountKeys;
    const receiverIndex = accounts.findIndex((a) => a.pubkey.toBase58() === expectedReceiver);
    
    if (receiverIndex === -1 || !tx.meta) return false;

    const preBalance = tx.meta.preBalances[receiverIndex];
    const postBalance = tx.meta.postBalances[receiverIndex];
    const balanceDiff = postBalance - preBalance;

    return balanceDiff >= expectedLamports;
  }
}

Penanganan Dead-Letter Queue (DLQ) dan Payload Anomalous

Kegagalan proses webhook terbagi menjadi dua kategori: transient errors (timeout RPC, lock database) dan terminal errors (signature salah, mismatch wallet, manipulasi nominal).

  • Transient Errors: Dikembalikan ke queue dengan skema exponential backoff (maksimal 5 kali retry dalam rentang 15 menit).
  • Terminal Errors: Jika transaksi tidak ditemukan pada level finalitas Solana setelah ambang batas toleransi (misalnya 60 detik pasca payload dipancarkan), job dialihkan ke Dead-Letter Queue (DLQ).

DLQ mencatat raw_payload, error_trace, dan signature ke tabel audit non-transaksional. Operator infrastruktur komputasi dapat memantau tabel ini untuk mendeteksi anomali transmisi atau re-play attack yang dilakukan oleh aktor tidak sah.

Trade-offs dan Pertimbangan Teknis

Peringatan Keamanan: Jangan mengandalkan data harga atau kuantitas unit GPU murni dari metadata webhook payload. Nilai nominal, target recipient, dan token address wajib divalidasi silang terhadap RPC Node Solana internal sebelum resource server dialokasikan.

  • RPC Node Cost vs Latency: Melakukan getParsedTransaction pada setiap event webhook meningkatkan utilisasi node Solana RPC. Untuk cluster high-throughput, gunakan Dedicated Geyser Plugin (gRPC streaming) daripada standard HTTP RPC poller.
  • Isolation Level: Penggunaan query SELECT ... FOR UPDATE menimbulkan overhead locking pada tabel gpu_instances. Namun, langkah ini wajib diterapkan untuk mencegah race condition double-credit akibat request paralel dari satu klien.