Skip to main content

Offline Relayer

The offline relayer (ghost-offline-relayer) provides gasless, sponsored transaction submission for the Specter network. It enables users who do not hold GHOST (the native gas token) to submit reveal transactions through the Ghost Protocol by having the relayer pay gas on their behalf.

Problem

The Ghost Protocol's reveal flow requires the recipient to submit an on-chain transaction that includes a ZK proof, nullifier, and other public inputs. This transaction consumes gas, payable in GHOST. However, several legitimate use cases involve recipients who have no GHOST balance:

  • NFC card redemptions: A user taps an NFC card to claim tokens. They may be new to Specter and have never acquired GHOST.
  • First-time users: Someone receiving their first private transfer on Specter cannot pay gas to reveal it.
  • Non-custodial flows: The sender committed tokens for a recipient who may not even have a Specter wallet yet.

Without the offline relayer, these users would need to first obtain GHOST from the faucet, wait for confirmation, and then submit the reveal — adding friction to what should be a seamless experience.

How It Works

User                           Offline Relayer                  Specter Chain
│ │ │
│ POST /reveal │ │
│ {proof, publicSignals, │ │
│ recipient, ...} │ │
│─────────────────────────────────▶│ │
│ │ │
│ │ Validate proof inputs │
│ │ Verify recipient address │
│ │ │
│ │ CommitRevealVault.reveal( │
│ │ proof, publicSignals, ────▶│
│ │ recipient, ...) │
│ │ │
│ │ Gas paid by operator wallet │
│ │ │
│ │◀── tx receipt ────────────────│
│◀── {txHash, status} ────────────│ │
│ │ │
│ Tokens revealed to recipient │ │

The user provides all the inputs needed for a reveal transaction, but instead of submitting it themselves (which would require gas), they send it to the offline relayer. The relayer constructs and submits the transaction using its own operator wallet, paying gas from its own GHOST balance.

API

POST /reveal

Submits a sponsored reveal transaction.

Request:

{
"proof": {
"pi_a": ["0x...", "0x...", "1"],
"pi_b": [["0x...", "0x..."], ["0x...", "0x..."], ["1", "0"]],
"pi_c": ["0x...", "0x...", "1"]
},
"publicSignals": ["0x...", "0x...", "0x...", "0x...", "0x...", "0x..."],
"recipient": "0x...",
"tokenAddress": "0x...",
"amount": "1000000000000000000"
}

Response (success):

{
"success": true,
"txHash": "0x...",
"blockNumber": 883500,
"gasUsed": "350000"
}

Response (error):

{
"error": "Nullifier already spent",
"code": "NULLIFIER_USED"
}

Validation

Before submitting the transaction, the offline relayer performs pre-flight validation to avoid wasting gas on transactions that would revert:

  1. Nullifier check: Query the NullifierRegistry contract to verify the nullifier has not already been spent. A double-reveal would revert on-chain.
  2. Root validity: Verify the Merkle root in the public signals matches a valid on-chain root (current or recent).
  3. Proof format: Validate that the proof components (pi_a, pi_b, pi_c) have the correct structure and field element ranges.
  4. Recipient address: Verify the recipient is a valid Ethereum address.

If any validation fails, the relayer returns an error without submitting a transaction.

Operator Wallet

The offline relayer uses a dedicated operator wallet funded with GHOST for gas payments. This wallet:

  • Is distinct from the operator wallets used by other services (root updater, faucet, etc.) to maintain privilege isolation.
  • Only needs the reveal() function permission on the CommitRevealVault (if the contract enforces caller restrictions; otherwise, any address can call reveal()).
  • Should be monitored for low balance conditions. If the operator runs out of gas, sponsored reveals stop working.

Gas Economics

Each reveal transaction costs approximately 300,000–400,000 gas. On the Specter testnet, gas prices are minimal, so the cost per sponsored reveal is negligible. In a production environment, the economics of who funds the operator wallet would need to be addressed — possibilities include:

  • The protocol treasury funding the relayer.
  • The sender pre-paying gas as part of the commit flow.
  • A fee deducted from the revealed tokens.

Rate Limiting

To prevent abuse of the gas sponsorship, the offline relayer enforces rate limits:

LimitValue
Per-IP request rate5 requests/minute
Per-recipient rate10 reveals/hour
Global rate60 reveals/minute

These limits ensure that a single actor cannot drain the operator wallet by submitting a high volume of sponsored reveals.

NFC Card Redemption Flow

The primary use case for the offline relayer is NFC card redemptions. The flow works as follows:

  1. A sender commits tokens to the Ghost Protocol, encoding the commitment secret into an NFC card.
  2. A recipient taps the NFC card with their phone, which reads the commitment secret.
  3. The recipient's device generates a ZK proof (via the proof relayer) using the commitment secret.
  4. The device submits the proof to the offline relayer, which reveals the tokens to the recipient's address.
  5. The recipient receives the tokens without ever holding GHOST for gas.

This creates a seamless experience where physical NFC cards function as bearer instruments for private token transfers.

Error Handling

  • Transaction revert: If the on-chain reveal reverts despite pre-flight validation (e.g., due to a race condition with another reveal), the error is returned to the client. No retry is attempted because the likely cause is a spent nullifier.
  • Nonce collision: The relayer maintains a local nonce counter with mutex locking to prevent concurrent reveals from using the same nonce.
  • Operator balance exhaustion: When the operator balance falls below a configurable threshold, the relayer returns 503 Service Unavailable for new requests and logs an alert.

Configuration

ParameterDescription
RPC_URLSpecter chain RPC endpoint
OPERATOR_PRIVATE_KEYOperator wallet private key (funded with GHOST)
COMMIT_REVEAL_VAULT_ADDRESSAddress of CommitRevealVault contract
NULLIFIER_REGISTRY_ADDRESSAddress of NullifierRegistry contract
COMMITMENT_TREE_ADDRESSAddress of CommitmentTree contract
MIN_OPERATOR_BALANCEMinimum GHOST balance before pausing (default: 100 GHOST)