Skip to main content

Epicbox relay protocol

Epicbox is the store-and-forward relay that carries slates between wallets that cannot reach each other directly, which is what makes a transfer work between two wallets behind NAT.

Protocol version: 3.0.0.

What a relay does

It accepts signed slate blobs addressed to a public key, stores them, and delivers them when the recipient connects and proves ownership of that key.

It never receives key material, and its only cryptographic operation is verifying signatures.

A relay sees the public keys it routes between, when they communicate, and the IP addresses they connect from. Amounts are inside the slate's commitments and are not visible to it.

Transport and addressing

WebSocket over TLS, port 443 by default. Messages are JSON objects with a type field.

Addresses are <public_key>@<domain>[:<port>], where public_key is 52 base58-check characters encoding a compressed secp256k1 public key. See addresses.

The key is derived per account, so a wallet holds one epicbox address for each account it has, and a listener subscribes with the address of the account it opened (impls/src/adapters/epicbox.rs:145). See accounts.

The wallet's default relay domain is epicbox.epiccash.com.

Authentication

Ownership of an address is proved by signing a challenge. The private key never leaves the wallet.

  1. On connection, and then on a repeating interval, the relay sends Challenge with a random string.
  2. The client replies Subscribe with its address and a signature over that challenge.
  3. The relay verifies the signature. A valid signature proves ownership and the client may receive the queue for that address.

The challenge interval is 60 seconds by default, which sets the worst case for how long a queued slate waits before a connected client is offered it. A client already connected and registered as a listener receives slates immediately instead, without waiting for the next challenge.

Clients that announce protocol version 2.0.0, or announce none, are issued a fixed challenge string instead of a random one, for compatibility with wallets older than 3.5.2.

Message types

Client to relay

TypeFieldsPurpose
Subscribeaddress, ver, signatureProve ownership and begin receiving
UnsubscribeaddressStop receiving and close
PostSlatefrom, to, str, signatureSubmit a slate for delivery
Madeaddress, signature, ver, epicboxmsgidAcknowledge a slate as processed
ClientDetailswallet_version, wallet_mode, protocol_versionAnnounce client identity
ping / pongnoneKeepalive

Challenge, GetVersion and FastSend are accepted for compatibility and are not part of 3.0.0.

A client announcing wallet_mode of listener is registered for immediate delivery, so slates arriving while it is connected are pushed straight through.

Relay to client

TypeFieldsPurpose
ChallengestrSign this to subscribe
OknoneAccepted
Slatefrom, str, signature, challenge, ver, epicboxmsgidA slate for you
Errorkind, descriptionRejected

Error kinds: UnknownError, InvalidRequest, InvalidSignature, InvalidChallenge, TooManySubscriptions.

Posting a slate

PostSlate is validated twice before storage: the address format of both from and to, and the signature over the payload against the key in the from address.

A slate whose destination domain and port match the relay's own configuration is stored locally. Any other destination is forwarded to that relay over a new outbound WebSocket connection, which is how cross-relay delivery works.

Delivery and its limits

Delivery is acknowledged rather than fire-and-forget, which is the main change from protocol 2.0.0.

  1. On a valid Subscribe, the relay sends the oldest undelivered slate for that key, with an epicboxmsgid.
  2. The client processes it and replies Made, signing over the epicboxmsgid.
  3. Only on a valid Made is the slate marked delivered and the next one released.

The queue is therefore serial per recipient: one outstanding slate at a time. A client that stops acknowledging blocks its own queue.

Two limits apply, and the shorter one is not the storage expiry:

Storage expiry, 7 days. Slates are held for 604,800 seconds and then removed regardless of state.

Acknowledgement limit, roughly nine challenge rounds. The relay counts send attempts per socket. After three unacknowledged attempts it resets the counter and starts another round, and after three such rounds, nine attempts in total, it discards all undelivered slates for that recipient rather than only the one that was failing (app_mongo.js:366). At a 60-second challenge interval that is about nine minutes.

An accepted post is not a delivered slate

Neither limit notifies the sender. From the sender's side the slate was accepted and then stopped existing, while their inputs stay reserved until the transfer is cancelled. See what releases them.

Additional delivery characteristics:

Delivery state is per connection. Acknowledgement counters live with the socket, so a reconnect resets them, and separate relay instances behind a load balancer do not share them. The reference deployment pins a client to one instance by IP.

Forwarded slates are not stored locally. A slate bound for another relay is forwarded over a new outbound connection without being persisted by the relay that accepted it.

Client behaviour

The wallet's relay client:

  • Reconnects after a 5-second delay, and keeps retrying.
  • After posting a finalised transaction, polls the node's mempool once per second for up to four minutes.
  • Uses slate V2, so neither a payment proof nor ttl_cutoff_height is carried. See choose the transport accordingly.

Run a relay

The reference deployment runs two relay instances behind nginx, with MongoDB for storage and a Rust helper binary for signature and address verification.

git clone https://github.com/EpicCash/epic-epicbox-docker.git
cd epic-epicbox-docker
git submodule update --init --recursive
EPICBOX_DOMAIN=epicbox.example.com docker compose up -d --build

Point wallets at it:

[epicbox]
epicbox_domain = "epicbox.example.com"
epicbox_port = 443

Configuration comes from environment variables or default_config.json, with the environment taking precedence. The keys that matter: EPICBOX_DOMAIN, EPICBOX_PORT, MONGO_URL, CHALLENGE_INTERVAL (milliseconds, default 60000), DEBUG, STATS.

Production prerequisites:

The MongoDB indexes come from mongo-init.js. The compose file mounts it into /docker-entrypoint-initdb.d, so a fresh MongoDB volume gets the queue indexes and the TTL index on createdat automatically. If you point the relay at an existing MongoDB instead, create them yourself; the same commands are in the comment block at the top of app_mongo.js. Without the TTL index, slates never expire.

Set STATS to true. It is off by default. The hourly counters it exposes are in-memory and reset on restart, so scrape them if you want history.

The reference compose file publishes the two relay instances on host ports 8888 and 8889 and nginx on 8443. Map those to 443 in your own deployment, or edit the compose file.

Source

Next