Faucet proof-of-work specification, version 1
Faucet proof-of-work version 1 defines the requests accepted by
POST /v3/gas. Every successful request pays the server's configured PAYOUT_MIST,
defaulting to 1 SUI (1000000000 MIST). The protocol charges clients a configurable
amount of computation before the faucet executes a payout.
The client submits checkpointSeq, recipient, nonce, and hashHex, the
complete Argon2d output. To grind, it constructs the eight-field preimage defined
below. The server independently resolves the other five preimage fields from
protocol constants, its faucet address, and public chain state.
GET /v3/challenge returns those values and the current quote as a convenience.
A client repeatedly hashes the eight-field preimage from section 2 with
Argon2d. It interprets the first 8 output bytes as an integer. A value below the
threshold solves the request. Otherwise, the client increments nonce and
tries again.
1. Inputs
Submitted fields
POST /v3/gas requires all four fields below. Send them as JSON strings;
using decimal strings for u64 values avoids JSON number precision loss.
| Field | Wire format | Meaning |
|---|---|---|
recipient |
Sui address string | Payout recipient |
checkpointSeq |
u64 as a canonical decimal string |
Checkpoint used to construct the proof |
nonce |
u64 as a canonical decimal string |
Nonce that produced the winning hash |
hashHex |
Exactly 64 lowercase hexadecimal characters, without 0x |
Complete 32-byte Argon2d output, defined in section 3 |
Preimage fields
Three submitted fields, checkpointSeq, recipient, and nonce, are inputs
to the hash. The server validates them and resolves the other five preimage
fields independently. The table below lists all eight fields in hash order.
hashHex is the resulting hash, not a ninth preimage field.
| # | Field | Type | Where it comes from |
|---|---|---|---|
| 1 | domain |
string | The literal sui-faucet-pow/1 |
| 2 | chainId |
string | gRPC GetServiceInfo.chain_id, used verbatim |
| 3 | checkpointSeq |
u64 | Submitted checkpoint sequence, inside the freshness window |
| 4 | checkpointDigest |
base58 | That checkpoint's digest, used verbatim |
| 5 | randomBytes |
base64 | 0x8's random_bytes, read as of that checkpoint |
| 6 | faucetAddress |
address | The faucet's payout address (GET /health) |
| 7 | recipient |
address | Submitted payout recipient |
| 8 | nonce |
u64 | Submitted winning nonce |
Normalization
The server rebuilds the preimage from the three submitted preimage inputs.
The client's preimage must produce the same bytes. A submitted hash that passes
the threshold check but differs from the server's result returns invalid_proof.
Use these normalization rules to keep both encodings identical:
- Addresses:
0xfollowed by 64 lowercase hex characters. Short forms must be zero-extended on the left (0x2→0x0000…0002). - Integers (
checkpointSeq,nonce): decimal, no leading zeros, no sign, no separators. Zero is0. checkpointDigest: base58 of the 32-byte digest, used verbatim. gRPC returns the digest in this form, so it goes in unchanged. The length is 43 or 44 characters depending on the value, so do not validate against a fixed length.randomBytes: base64 of the bytes the chain returns, per RFC 4648 section 4, with+and/as the last two alphabet characters and=padding whenever the byte length requires it. The current beacon emits 48 bytes, which needs no padding, but a client that encodes raw bytes itself must not assume that. Encode gRPCrandom_bytesas base64 before building the preimage. When the chain had no Random object at that checkpoint, this field is the empty string and the line is empty.chainId: use gRPCGetServiceInfo.chain_idverbatim. It is the base58 genesis checkpoint digest, not a shortened hexadecimal identifier. Do not re-encode it or normalize its case.
The digest and the randomness are hashed together because they are unpredictable
in different ways. The digest cannot exist before its checkpoint but is assembled
by whoever builds that checkpoint, while the randomness is a threshold BLS
signature that no single party can bias. docs/server-design.md covers the pairing.
2. Preimage layout
Encode the eight fields from section 1 as UTF-8 and join them with one newline
(U+000A):
sui-faucet-pow/1
<chainId>
<checkpointSeq>
<checkpointDigest>
<randomBytes>
<faucetAddress>
<recipient>
<nonce>
This string is the password passed to Argon2d in section 3. There is no trailing newline.
No field may contain a newline, which is what makes the join unambiguous without length prefixes or escaping. Every field is confined to an alphabet that excludes it: hex for addresses, decimal for integers, base58 for the digest, base64 for the randomness. A checkpoint holding no randomness leaves line 5 empty and keeps the field count at eight.
The first line is the domain separator and the PoW version marker.
3. Hash
Use Argon2d with these parameters:
| Parameter | Value |
|---|---|
| Algorithm | Argon2d |
| Version | 0x13 (19) |
| Password | The preimage of section 2, UTF-8 |
| Salt | sui-faucet-pow-1, ASCII, the domain of line 1 with / replaced by - |
| Memory | 8192 KiB |
| Iterations | 1 |
| Parallelism | 1 |
| Output | 32 raw bytes |
The equivalent PHC parameter string is $argon2d$v=19$m=8192,t=1,p=1.
Submit all 32 output bytes as hashHex, exactly 64 lowercase hexadecimal
characters without a 0x prefix, whitespace, or a PHC parameter string.
The server compares every byte with its recomputed output, not just the eight
bytes used to check difficulty. Clients already have this output when they find
a winning nonce, so including it requires no additional Argon2 computation.
Version 1 uses Argon2d, not Argon2id or Argon2i. Several libraries expose
only the data-independent variants, so a client may need a third-party library.
docs/server-design.md explains the choice.
Version 1 fixes these parameters. Two servers using sui-faucet-pow/1
therefore produce identical hashes for identical preimages.
GET /v3/challenge repeats the parameters so clients can verify them. A client
must reject a challenge when any parameter differs from the implemented
version. Changing a parameter requires the version change described in
section 7.
4. Difficulty and threshold
Interpret the first 8 bytes of the 32-byte hash as a big-endian unsigned
64-bit integer, v. A solution is valid when v < threshold.
The server chooses the required difficulty, as section 5 describes. Difficulty is an integer, the expected number of hash attempts. The threshold follows from it:
threshold = floor(2^64 / difficulty)
A random hash succeeds with probability
floor(2^64 / difficulty) / 2^64, which differs from 1/difficulty only by
integer-rounding precision. The expected attempt count is therefore
approximately difficulty. This linear scale lets the server adjust work in
fine increments.
Implementations must compute the threshold with integer division. The numerator does not fit in 64 bits, so use a 128-bit integer type or a bignum. Floating-point division would let two implementations disagree on the final integer.
On the wire, difficulty is a decimal integer string that follows the integer
normalization rules in section 1. Both sides derive the threshold from that
exact value. A server rounds fractional internal values up before quoting or
enforcing them. Rounding up preserves ordering, so the quote guarantee in
section 5 also holds for wire values.
GET /v3/challenge returns the quoted threshold as a decimal string. Clients
that use this endpoint do not need to calculate the threshold and can grind
directly until v < threshold.
Domain of the threshold function
The function above is defined for 2 <= difficulty <= 2^48.
The upper bound is where the threshold stops being useful. threshold(2^48)
is 2^16, and beyond that the comparison loses granularity against a 64-bit
value. At a difficulty of 1 the threshold would be 2^64, which every hash
clears and which no 64-bit integer holds. Neither bound is a policy. The
difficulty a server enforces is governed by section 5.
Overshooting
Overshooting is always accepted. A hash that clears a harder threshold also
clears an easier one, and the server only checks v < threshold(required). A
client can aim above the quoted value to keep its proof fresh while the latest
checkpoint timestamp advances.
Extra work also buys extra time. The freshness window is sized from the difficulty a proof actually clears, so a client that grinds a margin earns a proportionally longer deadline, up to the window cap. See Freshness window.
5. Acceptance rules
The server accepts a submission only when all of these conditions hold:
recipientis a valid Sui address.checkpointSeqandnonceare canonical decimal integers in the unsigned 64-bit range.hashHexcontains exactly 64 lowercase hexadecimal characters. Missing or malformed fields returninvalid_requestbefore any chain read or Argon2 computation.- The claimed hash satisfies
v < threshold(required). Otherwise, the server returnsinsufficient_workbefore any chain read or Argon2 computation. - The server reads the chain identifier, latest indexed checkpoint, and submitted checkpoint
through gRPC. Each proof checkpoint includes its sequence number, digest, consensus timestamp,
and native randomness as of that checkpoint. Randomness comes from the most recent successful
update affecting
0x8, found with a descending transaction scan whose exclusive upper bound ischeckpointSeq + 1. The submitted sequence must matchcheckpointSeqand cannot exceed the latest indexed sequence. - The latest checkpoint timestamp must be within the configured chain-tip age of the server clock. This check fails closed when the chain, gRPC endpoint, or server clock is stale. The submitted checkpoint's timestamp cannot be greater than the latest checkpoint's timestamp.
- The submitted checkpoint is no older than
POW_MAX_WINDOW_SECONDS, as measured bylatest.timestamp - submitted.timestamp. This rejects proofs that cannot be fresh before the server performs the expensive hash. - The server rebuilds the preimage with the returned
checkpointDigestandrandomBytes. An unpruned scan that reaches checkpoint zero without a transaction affecting0x8establishes that no Random object existed; only then israndomBytesthe empty string. RPC errors, unsupported update kinds, and malformed or incomplete checkpoint data returnnot_ready. Failed or scan-limited reads are never treated as absent randomness. - The tuple
(checkpointSeq, recipient, nonce)is neither reserved nor spent. This tuple determines the preimage because every other field is a protocol constant, checkpoint value, or server identity. The server reserves it after the request-time chain checks and before computing the hash. A successful payout stores its transaction digest. A payout whose submission may have occurred also stores its digest, as described in section 6. - The hash of the server-rebuilt preimage matches all 32 claimed output bytes
using a constant-time comparison. A mismatch returns
invalid_proofwithout disclosing the computed hash or whether it meets difficulty. The server never trusts a client-supplied preimage. - The submitted checkpoint is no older than the
windowSecondsearned by the verified hash.
Requiring the full output makes blind nonce submissions infeasible as a payout strategy, assuming the Argon2 output is unpredictable. A fabricated low-valued output can still pass rule 2 and incur one expensive verification before rule 8 rejects it. This requirement does not replace request admission limits.
Required difficulty
The server enforces the integer configured by POW_BASE_DIFFICULTY for every
request. GET /v3/challenge returns that same value as difficulty and
difficultyBase. The quote therefore equals the value enforced when the proof
is submitted.
Payout amount
The server reads PAYOUT_MIST at startup as a positive decimal u64, defaulting to
1000000000. Challenges advertise it as the decimal string amountMist; success
responses report the amount paid. Clients must read this field rather than assume
1 SUI. Payout requests cannot choose the amount.
The payout amount is not a preimage field. Configuration changes do not alter the hash or proof version, and a proof does not bind the server to an earlier quote. The server's startup configuration determines the payout when it accepts the proof.
Freshness window
GET /v3/challenge returns windowSeconds alongside the static difficulty.
A proof that exceeds the required work receives more time to land.
The window uses the difficulty the proof actually clears, with the required difficulty as its minimum and the configured maximum as its upper bound:
effective = min(max(achieved, required), maxDifficulty)
windowSeconds = min(max(baseWindow * effective / base, baseWindow), maxWindow)
achieved is floor(2^64 / v), and a v of zero counts as
maxDifficulty. Before the maximum window applies, the number of proofs an
attacker can stockpile is window × hashrate / difficulty. Substituting the
linear window formula reduces this expression to
baseWindow × hashrate / base, independent of difficulty.
Two consequences apply to clients:
windowSecondsrecords the freshness allowance at challenge time. The latest checkpoint timestamp continues to advance after the response.- A client that grinds past the quoted difficulty earns a longer deadline in proportion, until the maximum window applies.
6. Submitting
POST /v3/gas
Content-Type: application/json
{
"recipient": "0x1111111111111111111111111111111111111111111111111111111111111111",
"checkpointSeq": "1713342",
"nonce": "201",
"hashHex": "0008faf113e0b071f5f9d5d6e1dc9321890ecb7e81059f07dedbd3dec5e902bb"
}
Success:
{
"status": "success",
"digest": "…",
"recipient": "0x…",
"amountMist": "1000000000",
"difficulty": "724"
}
amountMist in the response is the amount paid. difficulty is the value the
server enforced, which may sit below the quote the client ground to.
Failures carry a machine-readable code. Failures that depend on chain state
can include a fresh challenge, as listed below. Cheap rejection paths do not
perform a chain read to attach a challenge.
| Code | Status | Meaning | Fresh challenge |
|---|---|---|---|
invalid_request |
400 | Malformed or out-of-range field | |
insufficient_work |
400 | Claimed hash does not meet the required difficulty. Includes requiredDifficulty; no chain read or Argon2 computation occurs |
|
invalid_proof |
400 | Claimed hash differs from the recomputed output. Includes preimageSha256 for diagnosing preimage differences |
|
stale_checkpoint |
409 | Checkpoint unavailable, ahead of the latest checkpoint, or outside the acceptance window | yes |
already_used |
409 | Tuple is reserved or spent. Includes the transaction digest when submission may have occurred |
yes |
overloaded |
503 | Too many verifications queued, or V8 heap pressure prevents a new replay reservation. Existing live proofs remain retained | |
funds_unavailable |
503 | The faucet cannot cover the payout or gas. The public message is generic service unavailability; operator logs and metrics retain the funding cause. No payout executed | |
not_ready |
503 | A chain read failed, returned incomplete data, or reported a latest checkpoint too far from the server clock. Retry after chain reads recover | |
payout_failed |
502 | Transaction construction failed or execution definitively failed on-chain. A digest means execution occurred and the proof stays spent. Inspect it, then request a new challenge before another payout |
|
payout_status_unknown |
502 | Submission or finality could not be confirmed. Includes the precomputed transaction digest. Do not request another payout until that digest is resolved |
|
internal |
500 | Unhandled server error |
Unhandled errors return status 500 with code internal. Future protocol
versions may add codes, so clients should handle known codes explicitly and
treat unknown codes as non-recoverable failures.
invalid_proof responses include preimageSha256, the SHA-256 of the
server-rebuilt preimage as lowercase hex. Compare it against the SHA-256 of the
client's preimage: a mismatch identifies a difference in inputs or encoding.
A match means the supplied Argon2 output is incorrect for that preimage.
Neither invalid_proof nor insufficient_work includes the server's Argon2
output. An invalid_proof response should be investigated rather than
automatically retried with a newly ground nonce.
already_used responses include the transaction digest once submission may
have occurred. A client that lost the connection after submitting can resubmit
the same four fields and recover that digest from the 409. The client must look
up the digest before requesting another payout. An already_used response
without a digest means the same tuple is still being processed before its
submission status is known.
Resubmitting a proof
A tuple is reserved when it passes rule 7 of section 5, and a submission
releases only the reservation it made itself. A submission rejected with
already_used leaves the existing reservation untouched.
The server releases a reservation after a failure that has no transaction
digest. A client that receives payout_failed without a digest, overloaded, or
funds_unavailable may resubmit the same nonce and hash once the condition clears,
provided the checkpoint remains inside the freshness window. funds_unavailable
clears after the faucet address balance is refilled.
A successful payout leaves the tuple spent and stores its digest. Any payout error with a digest does the same. For a definitive on-chain failure, the digest proves that execution occurred even though the payout effects were aborted. The client must inspect that transaction, then request a new challenge before grinding another proof.
The server computes the transaction digest before submission. If the submission
response is lost, it queries that digest to recover the result. A failed lookup
leaves the outcome uncertain because the transaction may already have executed.
The server returns payout_status_unknown with the digest and retains the proof
tuple. The same rule applies when successful execution is known but finality
confirmation fails. The client must resolve an unknown digest to a known outcome
before requesting another payout.
7. Versioning
The version appears in the first preimage line (sui-faucet-pow/1), the salt,
and the version field returned by GET /v3/challenge. Clients must reject
unrecognized versions.
The version fixes the preimage layout, hash function, hashing parameters, and
threshold rule. Changing these rules requires a new version because existing
clients would otherwise produce invalid proofs. The payout amount is deployment
configuration advertised through amountMist, not a versioned hashing parameter.
The number in sui-faucet-pow/N is the proof-of-work version. The v3 in the
HTTP path is the faucet API version. The two are independent.
8. Conformance vectors
pow-vectors.json provides inputs, exact preimages, thresholds, and expected Argon2d outputs. It covers checkpoints with and without randomness. Implementations can use these vectors to verify field joining, normalization, and hashing without contacting a faucet.