🧬 SteemBiota — A Deep Dive for Steem Developers

in Steem Dev6 hours ago

5% of the rewards of this post are for @steem.amal

1000111222.jpg

If you have ever wondered how far you can push Steem as an application platform — no L2, no sidechain, no backend — SteemBiota is a concrete answer.

It is a fully on-chain NFT ecosystem and life simulation running as a zero-build, client-side-only dApp. Every state transition is a Steem post or reply. Every ownership record is derived deterministically from chain history. The app ships as flat HTML + JS files on GitHub Pages and talks to the Steem RPC directly from the browser.

Here is how it actually works under the hood.


🏗️ Architecture: No Backend, No Build Step

The entire stack is CDN-loaded at runtime:

LayerChoiceWhy
Blockchain I/Osteem-js + KeychainNative Steem ops, no wallet custody
UIVue 3 + Vue Router 4 (CDN)Reactive without a bundler
Snapshot storageIPFS (Cloudflare / ipfs.io / Pinata)Content-addressed, verifiable
Local cacheIndexedDB (idb-style raw API)Offline-capable, zero deps
HostingGitHub PagesStatic, free, no server

No package.json. No Webpack. No SSR. The source files — index.html, blockchain.js, state.js, components.js, accessories.js, upload.js, app.js — are what ship.


🔗 Using Steem as an NFT Ledger

Every creature and accessory is identified by its canonical author/permlink key and treated as an NFT. Minting is a getDiscussionsByCreated-discoverable top-level post tagged steembiota. Lifecycle events (feed, play, walk, breed) and ownership operations (transfer offer/accept/cancel, wear on/off) are Steem replies to that post, written via comment operations signed by Keychain.

The two-sided ownership handshake deserves particular attention:

  1. The current owner posts a transfer_offer reply containing { type: "transfer_offer", to: "recipient" } in json_metadata.
  2. The recipient posts a transfer_accept reply. Only then does the GSM record the ownership change.

This means no one can force an NFT onto another account. It is anti-dumping by protocol design, not by application-layer policy.


⚙️ The Global State Machine (GSM)

Rather than scanning reply chains on every page render — which would mean hundreds of RPC calls per creature view — SteemBiota maintains a single Global State Machine: an in-memory object that is the authoritative source of truth for all ownership and equip state across a session.

// Vue-reactive ref driving the UI:
{
  version:   1,
  block_num: Number,   // last processed Steem block
  timestamp: String,   // "YYYY-MM-DDTHH:mm:ss" (UTC, no Z, no ms)
  ownership: {},       // "author/permlink" → current owner username
  equipped:  {}        // "accId" → "creatureId"
}

// Module-level Map — kept outside the reactive ref:
_nftRegistry           // "author/permlink" → { type: "creature"|"accessory" }

_nftRegistry intentionally lives outside the Vue reactive ref. At 100k+ entries, placing it inside the reactive object forces Vue to re-observe the entire tree on every Feed or Wear operation. The Map is serialised into the exported snapshot at checkpoint time so IPFS consumers always receive a complete registry field.

All transitions flow through a single applyOperation(state, op, blockNum, timestamp) function that is deterministic and idempotent — replaying the same event twice has no effect. This makes the GSM safe to seed from a snapshot and replay forward from any cutoff point.


🚀 Boot Sequence

Every page load runs an eight-step async bootstrap, surfaced as a syncStatus Vue ref (rendered as a slim status banner):

  1. IndexedDB check — load a previously verified snapshot instantly, zero RPC calls.
  2. On-chain checkpoint discovery — deep paginated scan of account history for custom_json ops with id steembiota_checkpoint, across all community publisher accounts (not just @steembiota).
  3. CID comparison — if the on-chain checkpoint CID matches the IDB snapshot CID, skip the IPFS download entirely.
  4. IPFS fetch + SHA-256 verification — three-gateway waterfall (Cloudflare → ipfs.io → Pinata) with a 5 s connect timeout and 30 s body timeout per gateway. The downloaded JSON is hashed and compared against state_hash in the checkpoint. Mismatch = tampered file = rejected.
  5. IDB persist — write the verified snapshot to sb_state_snapshot store (DB version 3).
  6. Exhaustive replay — cursor-based getDiscussionsByCreated paginator walks backwards until the snapshot cutoff. Reply-based ops are collected via a two-pass account-history scan: Pass 1 discovers transfer_offer recipients not yet in the known author set; Pass 2 scans the augmented set for all reply types. This closes the gap where a first-time transfer recipient's transfer_accept would otherwise be silently missed.
  7. Single atomic state write — replay runs against a local variable; stateRef.value is assigned exactly once at the end, eliminating UI flicker.
  8. Multi-tab handoff — a localStorage boot lock (post-write read-back for best-effort race mitigation) ensures only one tab runs bootstrapState(). Follower tabs receive the finalised state via BroadcastChannel, storage event, or 500 ms polling fallback, in that order.

1000111223.jpg

📸 Checkpoint System

The checkpoint system is how the app avoids a full chain replay on every cold boot. Any logged-in user can publish a checkpoint:

{
  "id": "steembiota_checkpoint",
  "json": {
    "version": 1,
    "block_num": 105521214,
    "state_hash": "<sha256-of-canonical-payload>",
    "snapshot_cid": "<ipfsv1-cid>"
  }
}

Canonical payload is a strict five-field object built by _buildSnapshotPayload(state, registry):

{
  version, block_num, timestamp,
  registry,   // compact: { "author/permlink": { t: "c"|"a", o: "owner" } }
  equipped    // "accId" → "creatureId"
}

All three code paths that produce a snapshot — hashState(), autoUploadCheckpoint(), and generateExport() — delegate to this single helper, ensuring the hashed object, the uploaded object, and the exported file are byte-for-byte identical. Keys are recursively sorted before serialisation for cross-engine hash stability.

Checkpoint scoring groups candidates by (block_num, state_hash) and scores each group: a large trust bonus for @steembiota, a floored integer reputation score (floating-point normalisation avoids JS-engine divergence), and a floor(block_num / 1000) recency bonus. The top-scoring group wins.

Root validationCHECKPOINT_ROOTS is a hard-coded array of genesis anchors. Any candidate checkpoint whose block_num falls at or below a root entry but whose state_hash does not match is rejected unconditionally, closing the poisoned-checkpoint attack surface.


🎨 Deterministic Procedural Rendering

Creature and accessory visuals are generated from a compact genome using a seeded PRNG. The same genome always produces the same canvas painting on any browser, any OS, any hardware. This matters: the genome is immutable (published on-chain once), but the rendering happens client-side on every view, so determinism is not optional.

Genomes encode body shape via MOR (Mean Optimal Ratio) matching — fitMor() performs a full linear scan of 10,000 MOR values to find the globally optimal body aspect ratio for the creature's proportions. Colour, limb counts, and trait expressions are all derived from the genome seed, not stored anywhere.


🔁 RPC Resilience

callWithFallback wraps every steem-js API call with a three-node rotation (api.steemit.com, api.steem.fans, api.justyy.com), a 12 s per-call timeout, and a full circuit-breaker: after exhausting all nodes, currentRPCIndex resets to 0 and an exponential backoff (1 s → 2 s → 4 s, capped at 30 s) prevents thundering-herd bursts. A successful call resets the backoff interval to 1 s.


🛠️ For Developers: What Is Reusable?

If you are building on Steem and any of this is useful, here is what transfers cleanly:

  • The GSM pattern — a single deterministic state object seeded from IPFS + replayed forward from chain history works for any on-chain application that needs O(1) state lookups without a backend.
  • callWithFallback — drop-in multi-node RPC wrapper with timeout and circuit-breaker.
  • The checkpoint systemcustom_json + IPFS + SHA-256 is a general tamper-evident snapshot pattern for any Steem dApp.
  • The two-pass reply scan — applicable to any app where reply authors can be unknown at scan time.
  • The boot lock patternlocalStorage post-write read-back + BroadcastChannel is a lightweight multi-tab sync mechanism that requires zero external dependencies.

📂 Source & Live App

Open source — all logic in seven flat JS/HTML files, no build step needed to read or fork.

Feedback, PRs, and community checkpoint publishers are all welcome. If you are experimenting with on-chain state machines or NFT patterns on Steem, let's compare notes.


Built for the Steem blockchain by @puncakbukit.

Assisted by https://Claude.ai/.

See also:

Sort:  

Upvoted! Thank you for supporting witness @jswit.