Hashing (Proof-of-Work)

Purpose: The single canonical reference for the Structs proof-of-work hashing mechanism. Covers the four struct-bound hash types, the universal input format, the algorithm, age-based difficulty, planet ore clocks, raid pause, the permission model, and the CLI/client surfaces. If you only read one PoW doc, read this one. Build-specific framing lives in building.md; guild founding is a paid transaction with no proof-of-work (see the structs-guild skill); strategy and job-tracking live in async-operations.md.


What Hashing Is and Why It Exists

Four struct-bound game actions are not finalized by a simple transaction — they are committed by a transaction and then finalized by submitting a valid proof-of-work hash:

Each of these is a two-step action: an initiate (or equivalent state change) that starts a clock, followed by a complete message that carries a proof and a nonce. The chain re-derives the hash from on-chain state plus your nonce, checks it equals your proof, and checks it clears the current difficulty. Only then does the action finalize.

These four are the only proof-of-work puzzles in the game. Founding a guild is a paid transaction (guild-create, 2,000,000 ualpha from the founder’s vault), not a hash race; the guild-charter query and the guildCharter* params still exist in state but nothing consumes them.

Hashing exists to make these high-value actions cost real work or real time, and to make that cost decay with age. A freshly initiated operation is effectively impossible to complete; an aged one is trivial. This is the engine behind the core tempo rule: initiate early, compute later. It is also what creates the ore-theft vulnerability window — refined Alpha is safe, but ore sits stealable for the entire ~34h refine clock. See resources.md and async-operations.md.


The Four Struct-Bound Hash Types

All four share one algorithm and one validator. They differ only in the hash-input keyword, the ID they key off, which clock supplies blockStart, and which parameter supplies the difficulty range.

Type Complete message Keyed on Clock lives on Difficulty range source Typical range
Build MsgStructBuildComplete structId the struct (blockStartBuild) StructType.BuildDifficulty 10–5,000 (per struct type)
Mine MsgStructOreMinerComplete structId the planet (planetBlockStartOreMine) StructType.OreMiningDifficulty 14,000 (Ore Extractor)
Refine MsgStructOreRefineryComplete structId the planet (planetBlockStartOreRefine) StructType.OreRefiningDifficulty 28,000 (Ore Refinery)
Raid MsgPlanetRaidComplete fleetId (+ target planetId) the planet (blockStartRaid) planet PlanetaryShield 25 base + defense contributions

The “difficulty range” is the tuning parameter the decay formula divides by. A higher range means difficulty decays more slowly (the operation stays hard for longer). It is not the difficulty itself.

Each complete message carries exactly two PoW fields plus the signer:

Field Type Meaning
creator address Signer (injected at broadcast)
structId / fleetId string The object being completed
proof string Lowercase hex SHA-256 digest of the hash input
nonce string The value that made the proof clear difficulty

Universal Input Format

Every hash type builds its input string by concatenating known fields with literal keyword separators — no delimiters, no JSON, no length prefixes. The block height is rendered as a base-10 string, the nonce is appended as a string.

Struct operations (build / mine / refine):

hashInput = {id} + {KEYWORD} + {blockStart} + "NONCE" + {nonce}

where {KEYWORD} is one of "BUILD", "MINE", "REFINE".

Raid (two IDs joined by @):

hashInput = {fleetId} + "@" + {planetId} + "RAID" + {blockStart} + "NONCE" + {nonce}

Mine and refine still key the input on structId, but {blockStart} is the planet clock, not a per-struct attribute. Completing any extractor on a planet resets that shared mine clock for every extractor on the planet.

Worked examples

Type Inputs Hash input string
Build struct 5-1, blockStart 1, nonce 42 5-1BUILD1NONCE42
Mine struct 14-5, planet clock 1283900, nonce 7 14-5MINE1283900NONCE7
Refine struct 15-5, planet clock 1290000, nonce 7 15-5REFINE1290000NONCE7
Raid fleet 9-5, planet 2-10, blockStart 1300000, nonce 7 9-5@2-10RAID1300000NONCE7

Universal format rules

Aspect Convention
Separators Literal keywords BUILD / MINE / REFINE / RAID and NONCE, concatenated with no extra delimiter
Raid @ between fleetId and planetId; the fleet id is the canonical 9-<decimal> with no zero-padding
Block height Decimal string (Go strconv.FormatUint(..., 10)) — no zero-padding
Nonce A string on the wire. The CLI/clients brute-force decimal integers ("1", "2", …)
Proof Lowercase hex SHA-256 digest, 64 chars, must equal the recomputed hash exactly (case-sensitive)
Encoding No base64, no raw bytes — proof and nonce are plain protobuf string fields

If you compute proofs with an external tool, you must reproduce this exact concatenation and lowercase-hex encoding, or the chain will reject the proof.


The Algorithm

hash       = sha256( utf8_bytes(hashInput) )        # single SHA-256
proofHex   = lowercase_hex(hash)                     # 64 chars, [0-9a-f]
isValid    = (submittedProof == proofHex)
             AND (first `difficulty` hex chars of proofHex are all '0')

Key facts:

The validator (HashBuildAndCheckDifficulty) returns both validity and the achievedDifficulty (how many leading zeros the digest actually had), which is emitted in the EventHashSuccess event.


Difficulty and Decay

Difficulty is age-based and drops logarithmically as the operation’s clock ages:

age = currentBlockHeight - blockStart

if age <= 1 or range <= 1:
    difficulty = 64                                  # effectively impossible
else:
    e          = max { k in 0..63 : range^k <= age^63 }   # exact integer arithmetic
    difficulty = 64 - e                              # never below 1

The difficulty cliff

Difficulty Hash time (rough)
≤ 8 seconds to minutes
9 hours
10+ effectively impossible

The jump between D=8 and D=9 is the single most important tactical fact in PoW. But even D=8 burns CPU. The recommendation is to wait for D=3, where the hash is trivially instant and zero CPU is wasted — the wait is the cost.

Time-to-difficulty table (6 sec/block)

Range Example D=8 D=5 D=3 (recommended)
200 Command Ship build ~11 min ~14 min ~17 min
250 Starfighter build ~12 min ~17 min ~20 min
10 Ore Extractor build ~1 min ~1 min ~1 min
700 Ore Refinery build ~34 min ~46 min ~57 min
2,880 PDC build ~2.0 hr ~2.9 hr ~3.7 hr
5,000 World Engine build ~3.2 hr ~4.9 hr ~6.4 hr
14,000 Mine ~8.1 hr ~12.7 hr ~17.2 hr
28,000 Refine ~15.0 hr ~24.4 hr ~33.7 hr

At range = 10 the curve is so short that difficulty is already 1 once the Extractor is 10 blocks old; -D is irrelevant for it.

To compute for any range and target D:

age_blocks   = ceil( range ^ ((64 - D) / 63) )      # smallest age with range^(64-D) <= age^63
time_seconds = age_blocks * 6

Worked example: fresh vs aged anchor

The same operation is impossible or instant depending only on how old its clock is. This is why timing dominates PoW.

So an old, abandoned anchor is a cheaper proof than a fresh one: initiate the moment you can, then come back later when the difficulty has decayed. A raid keyed to the target planet’s blockStartRaid works the same way — the longer that planet has been raid-vulnerable, the cheaper the raid proof.

Because the mine and refine clocks reset after every successful completion (see the per-type clock below), back-to-back mining of the same extractor re-enters the full decay each cycle. Repeat-mining is therefore naturally paced — you re-wait the decay every time, not just once.


The Per-Type Clock (blockStart)

The age that drives difficulty is measured from a clock specific to each operation. Knowing when each clock starts (and resets) tells you when a proof becomes cheap.

Type Clock field Lives on Starts / resets when
Build blockStartBuild struct Set when the struct is created (build initiated). One-shot per struct. The hash input uses blockStartBuild, NOT the current block height — anchoring on the current block produces an invalid proof.
Mine planetBlockStartOreMine planet Set when the first extractor on that planet goes online (oreMiningActiveQuantity 0→1). Additional extractors increment the quantity and leave the accrued age alone. Reset after each successful mine (any extractor) so every extractor on the planet re-enters the full decay. Deactivate decrements quantity; the clock is left in place.
Refine planetBlockStartOreRefine planet Same pattern as mining, with oreRefiningActiveQuantity.
Raid blockStartRaid planet Set when the defending Command Ship becomes raid-vulnerable. 0 means the planet is not raidable — a raid proof is rejected outright until the clock is armed (this prevents a trivial difficulty collapse). A completed (mined-out) planet is never raidable and cannot be reached by fleet-move.

Because mining and refining clocks reset every cycle, a long-running extractor/refinery re-ages from scratch after each completion — you re-wait the full decay each time. Because those clocks are shared per planet, completing on extractor A resets extractor B’s remaining wait too.


Mine/Refine Cycle Lifecycle

The mine and refine clocks are not just a difficulty input — they define a cycle whose lifecycle is easy to get exactly backwards. The rules (from x/structs/keeper/planet_cache.go and struct_cache.go):


Raid pause (mine and refine)

While a visiting fleet heads the planet’s raid queue (locationListStart != ""), mine and refine compute and complete are rejected with under_raid. The CLI prints planet (...) is under raid: mining is paused until the raid ends (refine equivalent).

Difficulty does not keep dropping during that window. When the queue empties, PauseOreClocksForRaid shifts each active planet ore clock forward by the paused interval (blockRaiderArrived → raid end), so pre-raid age is preserved. A clock that was first anchored during the raid lands at age zero.

Do not launch or retry struct-ore-mine-compute / struct-ore-refine-compute while the planet is under raid. Wait for the visitor to leave, then resume — the preserved age is still there.


Permissions Relating to Hashing

Hashing is not permissionless. Completing a struct-bound proof is gated at two independent layers, and each of the four types requires its own matching permission bit (not all four).

Layer 1 — Address permission (ante handler)

Before a complete message reaches its handler, the ante decorator checks the signing address holds the matching bit:

Message Required bit
MsgStructBuildComplete PermHashBuild
MsgStructOreMinerComplete PermHashMine
MsgStructOreRefineryComplete PermHashRefine
MsgPlanetRaidComplete PermHashRaid

An unregistered address, or a registered address lacking the bit, is rejected here.

Layer 2 — Object permission (keeper handler)

The handler then verifies the caller may act on the owner of the object via CanBuildHashedBy / CanMineHashedBy / CanRefineHashedBy / CanRaidHashedBy, each of which runs a PermissionCheck for the same single bit (PermHashBuild / PermHashMine / PermHashRefine / PermHashRaid). The check passes if the caller:

  1. is the owner of the object, or
  2. holds object-level permission for that bit on the owner, or
  3. meets the guild-rank threshold for that permission.

The permission bits

Bit Value Label Gates
PermHashBuild 1,048,576 hash_build Build completion
PermHashMine 2,097,152 hash_mine Mine completion
PermHashRefine 4,194,304 hash_refine Refine completion
PermHashRaid 8,388,608 hash_raid Raid completion
PermHashAll 15,728,640 — Composite of all four

A player’s primary address receives PermAll, so owners can always complete their own work. The interesting case is delegation.

Delegation recipe (open-but-granted hashing)

Because completing a proof requires only the relevant hash_* bit, you can hand the grind to a low-trust worker key without exposing your play/transfer permissions:

# Grant a worker address play + all hash permissions (and nothing else)
structsd tx structs permission-set-on-address cosmos1worker... 15728641 \
  --from primary --gas auto -y
# 15728641 = PermPlay (1) | PermHashAll (15728640)

That worker can now submit *-complete proofs on your behalf but cannot move tokens or restructure your account. See structs-permissions for the full delegation model.

Throttle

The ante also throttles proofs to one attempt per object per block (keyed by the struct/fleet ID). Spamming proofs for the same object in a single block is rejected.


CLI and Client Surfaces

CLI — compute vs complete

Command What it does Flags
struct-build-compute [struct id] Waits for difficulty to reach target, brute-forces the nonce locally, then auto-submits the complete message -D / --difficulty_target_start (1–64) + standard tx flags
struct-ore-mine-compute [struct id] Same, for mining same
struct-ore-refine-compute [struct id] Same, for refining same
planet-raid-compute [fleet id] Same, for raids same
struct-build-complete [struct id] [proof] [nonce] Submits a proof you computed externally standard tx flags (no -D)
struct-ore-mine-complete [struct id] [proof] [nonce] Manual mine completion same
struct-ore-refine-complete [struct id] [proof] [nonce] Manual refine completion same
planet-raid-complete [fleet id] [proof] [nonce] Manual raid completion same

The -D flag tells *-compute not to start hashing until difficulty has dropped to that level. The CLI polls block height and sleeps until the target is reached. Use -D 3 for instant, zero-waste hashing. The *-compute commands auto-submit minutes to days later, so they always run in the auto-approved form — see conventions and SAFETY.md.

Client — the webapp TaskManager

The web client implements the identical scheme: a TaskManager spawns a Web Worker per task that loops sha256(prefix + nonce) until the leading-hex-zero check passes, then submits the matching Msg*Complete (proof = the hex digest, nonce = decimal string). It builds the same input strings ({id}{KEYWORD}{blockStart}NONCE{nonce}, raid uses @), uses the floating-point form of the same difficulty formula (64 - floor(log10(age)/log10(range)*63)), and re-checks difficulty before submitting so a stale proof restarts rather than failing on-chain. Any conformant client — CLI, webapp, or your own bot — interoperates because the input format and algorithm are fixed.

Its wait policy is its own choice, not part of the protocol: the worker sleeps in 10-second polls until difficulty reaches 10 or below, rather than the -D 3 this guide recommends for the CLI. Both are conformant — a client may start hashing at any difficulty it likes. See develop/client/work-and-pow.md for the full client-side implementation.


Not Proof-of-Work (Disambiguation)

Several other game flows use the words “proof” or “hash” but are not this SHA-256 PoW:

Flow Mechanism Notes
Address registration secp256k1 signature over CHAIN{chainId}|REGISTERADDRESSPLAYER{playerId}ADDRESS{address}NONCE{n} A signature proof of key ownership with a per-address nonce (proofNonce on query structs address), not a difficulty hash
Guild-join proxy / signup secp256k1 signature over GUILD{guildId}ADDRESS{address}NONCE{n} (proofPubKey + proofSignature) Authorization, not PoW
Guild founding payment — 2,000,000 ualpha from the founder’s vault No proof field is accepted; legacy charter fields are rejected
Combat randomness (IsSuccessful) hash(blockHash, playerNonce) % denominator Uses hashing for RNG, but is not a submitted proof
Planet explore / allocation / player create (a native staking delegate from a fresh funded key) No proof at all Single-step transactions

The submitted-SHA-256-proof mechanism described in this doc is build, mine, refine, and raid. Address registration and proxy join are signatures, not difficulty hashes.


Source References

For verification against the chain and client implementations.

structsd (chain):

Concern Location
Hash + difficulty utilities x/structs/types/work.go — HashBuild, CalculateDifficulty, HashBuildAndCheckDifficulty
Build handler x/structs/keeper/msg_server_struct_build_complete.go
Mine handler x/structs/keeper/msg_server_struct_ore_miner_complete.go
Refine handler x/structs/keeper/msg_server_struct_ore_refinery_complete.go
Raid handler x/structs/keeper/msg_server_planet_raid_complete.go
Guild create (paid, no proof) x/structs/keeper/msg_server_guild_create.go, guild_creation_payment.go
Planet ore clocks + raid pause x/structs/keeper/planet_cache.go (OreMiningActivate, PauseOreClocksForRaid)
Object permission checks x/structs/keeper/player_cache.go — CanBuildHashedBy / CanMineHashedBy / CanRefineHashedBy / CanRaidHashedBy
Address permission map (ante) app/ante/maps.go (PoW message → PermHash*)
Proof throttle app/ante/throttle.go (ProofMessages)
Permission bit constants x/structs/types/permissions.go (PermHashBuild/Mine/Refine/Raid/All)
Compute CLI x/structs/client/cli/tx_struct_build_compute.go (and tx_struct_mine_compute.go, tx_struct_refine_compute.go, tx_planet_raid_compute.go)
Complete CLI (autocli) x/structs/module/autocli.go
Message protos proto/structs/structs/tx.proto (MsgStructBuildComplete, etc.)

structs-webapp (client):

Concern Location
Orchestration, queues, submission src/js/managers/TaskManager.js
Message construction, difficulty/age math src/js/models/TaskState.js
SHA-256 nonce loop (Web Worker) src/js/workers/TaskWorker.js
Per-type input prefix construction src/js/factories/TaskStateFactory.js
Complete message queueing src/js/managers/SigningClientManager.js

See Also