Skip to content
⌘K

Build a game

Voucher signing

How your game server signs item, state and trophy vouchers with its game key, the exact bytes that are signed, replay and expiry rules, key rotation and a test vector.

A voucher is a statement your game server signs: "this player earned this item", "this item's stats are now these", or "this player unlocked this trophy". The player's launcher submits it to the chain, and the contract checks your signature against the public key it holds for your game. Gas is paid by the game's budget where one exists.

Rare never holds your key and never signs for you. A leaked key can mint items up to the on-chain supply caps, so keep it on your server, out of git and out of the client.

The item and state formats below are the rare_items design, which is not published yet. The trophy format is the one in the Trophies contract. All three are signed the same way.

The game key

Studio generates an ed25519 key in your browser, offers the private key as a one-time download and puts only the public key on chain: in the item class (issuer) and the trophies game (set_issuer). Load the private key on your server:

TypeScript
import { gameKey } from "@rare/sdk/server";

const key = gameKey(process.env.RARE_GAME_KEY!);
console.log(key.publicKeyHex); // 32 bytes, the same value that is on chain

gameKey accepts a suiprivkey1... string, 64 hex characters (with or without 0x), 32 bytes of base64 or the raw bytes. A key of another scheme is refused.

Making vouchers

import { itemVoucher } from "@rare/sdk/server";

const v = await itemVoucher(key, {
  classId: process.env.CLASS_ID!,
  player,            // the player's address
  kind: 3,           // the kind id from add_kind
  attrs: { power: 12 },
  ttlMs: 600_000,    // default 10 minutes
});
res.json({ voucher: v.token });

Send v.token to the game client and call rare.claim(token). The token is the base64url of a JSON envelope with the fields and the signature, so it is safe in a URL (rare://claim?v=<token>) as well as in JSON.

What is signed

The signature is ed25519 over raw bytes: no hashing and no intent prefix. All integers are u64 little endian, addresses and object ids are 32 bytes, and bcs(keys) is the BCS encoding of a vector of strings, exactly what Move's bcs::to_bytes writes.

VoucherMessage
Item"RARE_ITEM_V1", class id, player, kind, bcs(keys), bcs(vals), nonce, expires_ms
State"RARE_ITEM_STATE_V1", item id, rev, bcs(keys), bcs(vals), nonce, expires_ms
Trophy"RARE_TROPHY_V1", registry id, player, game id, trophy id, nonce, expires_ms

The class id and registry id bind a voucher to one deployment, so a testnet voucher cannot be used on mainnet. A state voucher signs keys and vals and the contract writes only the keys it is given, so you can change one stat without re-signing the rest.

Replay, expiry and ordering

  • Expiry. expires_ms is checked against the chain clock. Keep it short. The default is 10 minutes.
  • Replay. The nonce is recorded per player. A voucher with a nonce the player already used is refused. The default nonce is 64 random bits, so two vouchers never collide. Pass your own nonce if you want vouchers you can reason about, but never reuse one for the same player.
  • Ordering. A state voucher carries rev and the contract requires it to equal the item's current rev, then increments it. A stale or replayed update cannot apply, and updates apply in order.
  • Player binding. Item and trophy vouchers name the player. Only that address can use them.

Verify what you sign

verifyVoucher checks a voucher the way the chain will. Use it in tests and in any service that accepts vouchers from another service.

TypeScript
import { verifyVoucher } from "@rare/sdk/server";

const v = await verifyVoucher(token, key.publicKey, { player });

It throws a RareVoucherError (code voucher_invalid) for a malformed token, a signature that does not verify, an expired voucher or the wrong player.

Rotating the key

Generate a new key in Studio and call set_issuer with its public key (on the item class and on the trophies game). Vouchers signed with the old key stop working at once. Vouchers still in flight fail with rejected, and the player simply asks the server for a new one. Setting an empty key turns vouchers off.

Test vector

Use this to check another language's implementation. The private key is 31 zero bytes and then 01.

Public key4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29
Class id0xc1 repeated 32 times
Player0x09 repeated 32 times
Kind, nonce, expires_ms3, 7, 1800000600000
Attributespower = 12
Message (hex)
524152455f4954454d5f5631
c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1
0909090909090909090909090909090909090909090909090909090909090909
0300000000000000
0105706f776572
01023132
0700000000000000
c0776518a3010000
Signature (hex)
0c429736bcdb62babb3ac80cca7108d51a3e02a46bf335c7ff401e067e1f2226fb12d888f34dc40101afdd19529552c7a91292f0343efd11e90ade19c96f3d0a

The line breaks in the message are only for reading. It is one byte string.

--
TPS:--