From 947de39785113c774fd51ac93e6b479f4f562569 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:11:26 -0300 Subject: [PATCH 1/7] feat(deploy): AtomicAssets Track B end-to-end Docker stack (Jungle 4) Stand up the Rust+WormDB ("v3") AtomicAssets serving lifecycle on Docker, fed by the Jungle 4 testnet: snapshot -> Mongo (snapshot-load --tables atomic) -> .wseg (aa-build) -> served by wormdb-server (lightapi + atomicassets domains composed). - deploy/atomicassets/docker-compose.yml: mongodb + aa-loader (one-shot) + wormdb-aa - Dockerfile.tools (Rust: snapshot-load + aa-build), Dockerfile.wormdb (slim runtime), build-wormdb.sh (the 4-repo Zig build, quic=false, into bin/wormdb) - entrypoints, wormdb.json (trusted-network: require_auth=false), README Chain source = the existing WSL Jungle 4 node (host.docker.internal:28888/:28080); no nodeos container. Mongo is the aa-build bridge AND the parallel state oracle (state stays dual-homed Mongo+WormDB while WormDB is proven; store-once is a later flip). Validated on the jungle4-v8 snapshot (block 269541093): 1552 assets / 188 templates / 48 schemas / 31 collections decoded with 0 errors; GET /atomicassets/v1/assets?owner=... returns assets newest-first, byte-matching the Mongo oracle. --- deploy/atomicassets/.env.example | 24 +++++++ deploy/atomicassets/.gitignore | 5 ++ deploy/atomicassets/Dockerfile.tools | 17 +++++ deploy/atomicassets/Dockerfile.wormdb | 11 +++ deploy/atomicassets/README.md | 49 +++++++++++++ deploy/atomicassets/build-wormdb.sh | 56 +++++++++++++++ deploy/atomicassets/docker-compose.yml | 91 ++++++++++++++++++++++++ deploy/atomicassets/entrypoint-loader.sh | 37 ++++++++++ deploy/atomicassets/entrypoint-server.sh | 15 ++++ deploy/atomicassets/wormdb.json | 7 ++ 10 files changed, 312 insertions(+) create mode 100644 deploy/atomicassets/.env.example create mode 100644 deploy/atomicassets/.gitignore create mode 100644 deploy/atomicassets/Dockerfile.tools create mode 100644 deploy/atomicassets/Dockerfile.wormdb create mode 100644 deploy/atomicassets/README.md create mode 100644 deploy/atomicassets/build-wormdb.sh create mode 100644 deploy/atomicassets/docker-compose.yml create mode 100644 deploy/atomicassets/entrypoint-loader.sh create mode 100644 deploy/atomicassets/entrypoint-server.sh create mode 100644 deploy/atomicassets/wormdb.json diff --git a/deploy/atomicassets/.env.example b/deploy/atomicassets/.env.example new file mode 100644 index 0000000..0d0f470 --- /dev/null +++ b/deploy/atomicassets/.env.example @@ -0,0 +1,24 @@ +# Copy to .env and adjust. `docker compose` auto-loads .env. + +# Host directory that CONTAINS the Jungle 4 snapshot .bin (mounted read-only at /snap). +# The WSL node keeps it at ~/chains/jungle (e.g. snapshot-…-jungle4-v8-0269541093.bin). +# Point this at a path your Docker host can bind-mount (a Windows path, or copy the .bin here). +SNAPSHOT_DIR=./snapshot + +# Chain name (Mongo db = _; snapshot-load derives block_num from the filename). +CHAIN=jungle4 +MONGO_PREFIX=hyperion + +# Comma-separated decoded data-attribute facets to inverted-index in the segment. +AA_DATA_FIELDS=rarity + +# Cap assets streamed into the segment for a fast first pass (0 = all). +ASSET_LIMIT=0 + +# Host port mappings. +MONGO_PORT=27018 +WORMDB_GATEWAY_PORT=6390 +WORMDB_WIRE_PORT=6389 + +# Mongo WiredTiger cache (GiB). +MONGO_CACHE_GB=2 diff --git a/deploy/atomicassets/.gitignore b/deploy/atomicassets/.gitignore new file mode 100644 index 0000000..49fa3bd --- /dev/null +++ b/deploy/atomicassets/.gitignore @@ -0,0 +1,5 @@ +# Out-of-band build output (the wormdb binary) + local snapshot + env. +bin/ +snapshot/ +.env +*.wseg diff --git a/deploy/atomicassets/Dockerfile.tools b/deploy/atomicassets/Dockerfile.tools new file mode 100644 index 0000000..b105421 --- /dev/null +++ b/deploy/atomicassets/Dockerfile.tools @@ -0,0 +1,17 @@ +# Rust tools image: snapshot-load (snapshot -> Mongo) + aa-build (Mongo -> .wseg). +# Build context = hyperion-tools repo root (the cargo workspace). +FROM rust:1-bookworm AS builder +WORKDIR /build +COPY Cargo.toml Cargo.lock ./ +COPY crates ./crates +# Pure-Rust deps (rs_abieos rust-backend, rustls, ruzstd, mongodb driver) -> no C toolchain needed. +RUN cargo build --release -p snapshot-load \ + && cargo build --release -p wseg-build --bin aa-build + +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates bash coreutils findutils && rm -rf /var/lib/apt/lists/* +COPY --from=builder /build/target/release/snapshot-load /usr/local/bin/snapshot-load +COPY --from=builder /build/target/release/aa-build /usr/local/bin/aa-build +COPY deploy/atomicassets/entrypoint-loader.sh /entrypoint-loader.sh +RUN chmod +x /entrypoint-loader.sh diff --git a/deploy/atomicassets/Dockerfile.wormdb b/deploy/atomicassets/Dockerfile.wormdb new file mode 100644 index 0000000..a0d39b9 --- /dev/null +++ b/deploy/atomicassets/Dockerfile.wormdb @@ -0,0 +1,11 @@ +# Runtime image for the wormdb-server binary (lightapi + atomicassets domains composed). +# The binary is built OUT-OF-BAND by ./build-wormdb.sh (a 4-repo Zig build) into ./bin/wormdb, +# then copied onto a slim runtime here — same pattern as the Light-API preview release. +# Build context = hyperion-tools repo root. +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates bash curl libsodium23 && rm -rf /var/lib/apt/lists/* +COPY deploy/atomicassets/bin/wormdb /usr/local/bin/wormdb +COPY deploy/atomicassets/wormdb.json /etc/wormdb.json +COPY deploy/atomicassets/entrypoint-server.sh /entrypoint-server.sh +RUN chmod +x /usr/local/bin/wormdb /entrypoint-server.sh diff --git a/deploy/atomicassets/README.md b/deploy/atomicassets/README.md new file mode 100644 index 0000000..ad0a051 --- /dev/null +++ b/deploy/atomicassets/README.md @@ -0,0 +1,49 @@ +# WormDB AtomicAssets — end-to-end stack (Track B / v3) + +Runs the **Rust + WormDB** AtomicAssets serving lifecycle on Docker, fed by **Jungle 4 testnet**: +`snapshot → Mongo → .wseg → serve` now (Phase 0–1), then a live SHiP feed (Phase 2) and a +Hyperion/`archive-server` history tier (Phase 3). See the design plan for the full roadmap. + +## Tiers (store each datum once, eventually) + +- **State** → WormDB `.wseg` + overlay **and** MongoDB (dual-homed *on purpose* while we prove WormDB — + the two are diffed; we don't blacklist Hyperion's AA indexing yet). +- **History index** → Hyperion `-action-*` with an `@atomic` enrichment (Phase 3). +- **History payloads** → cold `archive-server`, decoded lazily from the frozen ship logs (Phase 3). + +## Chain source + +The existing **Jungle 4 node in WSL** (`~/chains/jungle`, tmux): chain API `:28888`, SHiP `:28080`, with a +ready snapshot and the state-history logs. Containers reach it at `host.docker.internal:28888/:28080` +(the node has `http-validate-host` on — send a `Host` header). No nodeos container. + +## Phase 0 — bring up the state pipe + +```sh +# 1. Build the wormdb-server binary (4-repo Zig build: engine + lightapi + atomicassets domains). +./build-wormdb.sh # -> bin/wormdb (REPOS_PARENT=P:/ by default) + +# 2. Point at the snapshot + configure. +cp .env.example .env +# set SNAPSHOT_DIR to a host dir holding snapshot-…-jungle4-v8-*.bin + +# 3. Up: aa-loader (snapshot -> Mongo -> aa.wseg) then wormdb-aa serves it. +docker compose --profile state up --build + +# 4. Query. +curl "http://localhost:6390/atomicassets/v1/assets?owner=&limit=10" +``` + +## Layout + +| file | role | +|------|------| +| `docker-compose.yml` | `mongodb` + `aa-loader` (one-shot) + `wormdb-aa` (serve) | +| `Dockerfile.tools` | Rust image: `snapshot-load` + `aa-build` | +| `Dockerfile.wormdb` | slim runtime; copies the prebuilt `bin/wormdb` | +| `build-wormdb.sh` | builds `bin/wormdb` (reuses `wormdb-server/docker/build-linux.sh` flow) | +| `entrypoint-loader.sh` | `snapshot-load --tables atomic` → `aa-build` | +| `entrypoint-server.sh` | `wormdb --atomicassets-segment /data/aa.wseg` | +| `wormdb.json` | server/gateway config (segment attached via the CLI flag) | + +`bin/`, `snapshot/`, `.env`, `*.wseg` are gitignored. diff --git a/deploy/atomicassets/build-wormdb.sh b/deploy/atomicassets/build-wormdb.sh new file mode 100644 index 0000000..9d42933 --- /dev/null +++ b/deploy/atomicassets/build-wormdb.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Build the wormdb-server binary (with the lightapi + atomicassets domains composed) for Linux, +# into deploy/atomicassets/bin/wormdb. Reuses the proven 4-repo build flow (quic=false) from +# wormdb-server/docker/build-linux.sh: copy the four sibling repos (excluding caches), clone +# meshguard (the Windows symlink dep), then `zig build`. +# +# REPOS_PARENT=P:/ ./build-wormdb.sh # default REPOS_PARENT=P:/ (Docker Desktop on Windows) +# +# Needs: Docker, and the four repos as siblings under REPOS_PARENT: +# wormdb wormdb-server wormdb-domain-lightapi wormdb-domain-atomicassets +set -euo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +OUT="$HERE/bin" +mkdir -p "$OUT" +REPOS_PARENT="${REPOS_PARENT:-P:/}" +# Docker Desktop on Windows needs a Windows-style mount source; a git-bash path (/p/...) isn't +# translated (it would bind to a non-existent path inside the Linux VM). cygpath -m -> P:/... +OUT_MOUNT="$(cygpath -m "$OUT" 2>/dev/null || echo "$OUT")" + +echo "[build-wormdb] REPOS_PARENT=$REPOS_PARENT OUT=$OUT_MOUNT" +docker run --rm \ + -v "${REPOS_PARENT}:/host:ro" \ + -v "${OUT_MOUNT}:/out" \ + -v wormdb-zigcache:/tmp/zc \ + -v wormdb-ziggcache:/tmp/zgc \ + ubuntu:24.04 bash -c ' +set -e +ZIG_VER=0.16.0 +echo "=== tools ===" +apt-get update -qq +apt-get install -y -qq curl xz-utils git build-essential cmake libsodium-dev ca-certificates pkg-config >/dev/null 2>&1 +echo "=== zig ${ZIG_VER} ===" +cd /opt && curl -fsSL "https://ziglang.org/download/${ZIG_VER}/zig-x86_64-linux-${ZIG_VER}.tar.xz" -o zig.tar.xz && tar xf zig.tar.xz +export PATH="/opt/zig-x86_64-linux-${ZIG_VER}:${PATH}" +zig version +echo "=== copy 4 repos (sibling layout, excl caches/.git/node_modules) ===" +mkdir -p /work +for r in wormdb wormdb-server wormdb-domain-lightapi wormdb-domain-atomicassets; do + mkdir -p "/work/$r" + tar -C "/host/$r" --exclude=.zig-cache --exclude=zig-out --exclude=.git --exclude=node_modules -cf - . | tar -C "/work/$r" -xf - +done +echo "=== meshguard from github (replaces the windows symlink) ===" +rm -rf /work/wormdb/deps/meshguard +git clone -q https://github.com/igorls/meshguard /work/wormdb/deps/meshguard +echo "=== zig build (quic=false) ===" +cd /work/wormdb-server +zig build --cache-dir /tmp/zc --global-cache-dir /tmp/zgc +cp -v zig-out/bin/wormdb /out/wormdb +echo "=== smoke ===" +mkdir -p /tmp/d +/out/wormdb --port 6599 --gateway-port 6599 --data /tmp/d --persistence none >/tmp/srv.log 2>&1 & +SRV=$!; sleep 2 +grep "Composed domain" /tmp/srv.log || true +kill "$SRV" 2>/dev/null || true +' +echo "[build-wormdb] built: $OUT/wormdb" diff --git a/deploy/atomicassets/docker-compose.yml b/deploy/atomicassets/docker-compose.yml new file mode 100644 index 0000000..f788cb5 --- /dev/null +++ b/deploy/atomicassets/docker-compose.yml @@ -0,0 +1,91 @@ +# WormDB AtomicAssets — Track B end-to-end (Phase 0: state bootstrap from a Jungle 4 snapshot). +# +# 1. ./build-wormdb.sh # build the wormdb-server binary (4-repo Zig build) -> bin/wormdb +# 2. cp .env.example .env && edit # point SNAPSHOT_DIR at the dir holding the Jungle 4 snapshot +# 3. docker compose --profile state up --build +# +# Pipeline: aa-loader (snapshot -> Mongo via snapshot-load, then Mongo -> .wseg via aa-build) +# -> wormdb-aa serves the AtomicAssets HTTP API from the segment on :6390. +# +# Chain source = the existing Jungle 4 node (WSL ~/chains/jungle): reached at host.docker.internal:28888 +# (chain API) / :28080 (SHiP). No nodeos container. ES / Hyperion / archive-server arrive in Phase 3. + +name: aa-stack + +x-host-gw: &host_gw + - "host.docker.internal:host-gateway" + +services: + mongodb: + image: mongo:8 + container_name: aa-mongodb + command: ["--wiredTigerCacheSizeGB", "${MONGO_CACHE_GB:-2}"] + volumes: + - mongo-data:/data/db + ports: + - "${MONGO_PORT:-27018}:27017" + networks: [aa-stack] + profiles: ["state", "all"] + healthcheck: + test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 5s + + # One-shot: decode the snapshot's AtomicAssets state into Mongo, then build the faceted .wseg. + aa-loader: + build: + context: ../.. + dockerfile: deploy/atomicassets/Dockerfile.tools + image: aa-stack/tools:latest + container_name: aa-loader + command: ["/entrypoint-loader.sh"] + environment: + CHAIN: ${CHAIN:-jungle4} + MONGO_URI: mongodb://mongodb:27017 + MONGO_PREFIX: ${MONGO_PREFIX:-hyperion} + WSEG_OUT: /data/aa.wseg + AA_DATA_FIELDS: ${AA_DATA_FIELDS:-rarity} + ASSET_LIMIT: ${ASSET_LIMIT:-0} + volumes: + - ${SNAPSHOT_DIR:-./snapshot}:/snap:ro + - aa-data:/data + depends_on: + mongodb: + condition: service_healthy + networks: [aa-stack] + profiles: ["state", "all"] + restart: "no" + + # Serves the AtomicAssets HTTP API from the mmap'd segment (+ the in-DB overlay later). + wormdb-aa: + build: + context: ../.. + dockerfile: deploy/atomicassets/Dockerfile.wormdb + image: aa-stack/wormdb:latest + container_name: aa-wormdb + command: ["/entrypoint-server.sh"] + environment: + AA_SEGMENT: /data/aa.wseg + volumes: + - aa-data:/data + - ./wormdb.json:/etc/wormdb.json:ro + ports: + - "${WORMDB_GATEWAY_PORT:-6390}:6390" + - "${WORMDB_WIRE_PORT:-6389}:6389" + extra_hosts: *host_gw + depends_on: + aa-loader: + condition: service_completed_successfully + networks: [aa-stack] + profiles: ["state", "all"] + +volumes: + mongo-data: + aa-data: + +networks: + aa-stack: + name: aa-stack + driver: bridge diff --git a/deploy/atomicassets/entrypoint-loader.sh b/deploy/atomicassets/entrypoint-loader.sh new file mode 100644 index 0000000..9d4b62c --- /dev/null +++ b/deploy/atomicassets/entrypoint-loader.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# One-shot loader: decode the snapshot's AtomicAssets state into MongoDB (snapshot-load --tables +# atomic), then build the faceted .wseg segment from that Mongo state (aa-build). +set -euo pipefail + +CHAIN="${CHAIN:-jungle4}" +MONGO_URI="${MONGO_URI:-mongodb://mongodb:27017}" +MONGO_PREFIX="${MONGO_PREFIX:-hyperion}" +MONGO_DB="${MONGO_PREFIX}_${CHAIN}" # snapshot-load db name = _ +WSEG_OUT="${WSEG_OUT:-/data/aa.wseg}" +AA_DATA_FIELDS="${AA_DATA_FIELDS:-rarity}" +ASSET_LIMIT="${ASSET_LIMIT:-0}" + +# Prefer the EOS-Nation-named portable snapshot (its trailing digits are the block_num that +# snapshot-load auto-derives); fall back to any non-archive .bin. +BIN="$(find /snap -maxdepth 1 -name 'snapshot-*.bin' | head -1 || true)" +[[ -n "$BIN" ]] || BIN="$(find /snap -maxdepth 1 -name '*.bin' ! -name '*archive*' | head -1 || true)" +[[ -n "$BIN" ]] || { echo "[loader] ERROR: no snapshot .bin in /snap (mount SNAPSHOT_DIR)"; ls -la /snap; exit 1; } + +echo "[loader] snapshot=$BIN chain=$CHAIN -> $MONGO_URI db=$MONGO_DB" + +# 1) snapshot -> Mongo: atomicassets + atomicmarket state, decoded (seek path; the atomic preset +# needs a local .bin). Drops the AA/AM collections first for an idempotent re-run. +echo "[loader] === snapshot-load (--tables atomic) ===" +time snapshot-load --snapshot "$BIN" --tables atomic --chain "$CHAIN" \ + --mongo "$MONGO_URI" --mongo-prefix "$MONGO_PREFIX" --mongo-drop + +# 2) Mongo -> .wseg: the faceted AtomicAssets segment the Zig domain serves. +echo "[loader] === aa-build -> $WSEG_OUT ===" +AABUILD_ARGS=(--mongo-uri "$MONGO_URI" --db "$MONGO_DB" --out "$WSEG_OUT" --data-fields "$AA_DATA_FIELDS") +if [[ "$ASSET_LIMIT" =~ ^[0-9]+$ ]] && [[ "$ASSET_LIMIT" -gt 0 ]]; then + AABUILD_ARGS+=(--limit "$ASSET_LIMIT") +fi +time aa-build "${AABUILD_ARGS[@]}" + +echo "[loader] done:" +ls -la "$WSEG_OUT" diff --git a/deploy/atomicassets/entrypoint-server.sh b/deploy/atomicassets/entrypoint-server.sh new file mode 100644 index 0000000..0d85d03 --- /dev/null +++ b/deploy/atomicassets/entrypoint-server.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Serve the AtomicAssets HTTP API from the mmap'd .wseg segment. The wormdb-server binary already +# composes the lightapi + atomicassets domains; attaching a segment named `atomicassets` lights up +# the AA routes (the domain mounts under that segment name). +set -euo pipefail + +AA_SEGMENT="${AA_SEGMENT:-/data/aa.wseg}" +if [[ ! -f "$AA_SEGMENT" ]]; then + echo "[server] ERROR: segment $AA_SEGMENT missing — run aa-loader first" + exit 1 +fi +mkdir -p /var/lib/wormdb + +echo "[server] serving AtomicAssets from $AA_SEGMENT (gateway :6390, wormwire :6389)" +exec wormdb --config /etc/wormdb.json --atomicassets-segment "$AA_SEGMENT" diff --git a/deploy/atomicassets/wormdb.json b/deploy/atomicassets/wormdb.json new file mode 100644 index 0000000..418ba0d --- /dev/null +++ b/deploy/atomicassets/wormdb.json @@ -0,0 +1,7 @@ +{ + "data": "/var/lib/wormdb", + "store": { "persistence": "none" }, + "server": { "port": 6389 }, + "gateway": { "enabled": true, "port": 6390 }, + "auth": { "require_auth": false } +} From bb7ff73b247221cf1b0f2cb1efa3bd1067bf48a6 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 16:17:05 -0300 Subject: [PATCH 2/7] harden(deploy): bind aa-stack host ports to loopback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security review of the prior commit flagged the Mongo + WormDB host port mappings as off-host network exposure (Mongo has no auth; the gateway runs require_auth=false for this trusted-LOCAL proving deploy). Bind all host port mappings to 127.0.0.1 — inter-service traffic uses the aa-stack bridge (mongodb:27017 / aa-wormdb:6389) and is unaffected; only the host-side convenience mappings change. 6389 (WormWire) / 6390 (HTTP) documented inline. Deferred (matches the existing wormdb-server/docker/build-linux.sh pattern, not new here): pin the Zig tarball SHA-256 + a meshguard commit in build-wormdb.sh. --- deploy/atomicassets/docker-compose.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/deploy/atomicassets/docker-compose.yml b/deploy/atomicassets/docker-compose.yml index f788cb5..bfa255b 100644 --- a/deploy/atomicassets/docker-compose.yml +++ b/deploy/atomicassets/docker-compose.yml @@ -22,8 +22,10 @@ services: command: ["--wiredTigerCacheSizeGB", "${MONGO_CACHE_GB:-2}"] volumes: - mongo-data:/data/db + # Loopback-only: the loader/server reach Mongo over the aa-stack bridge (mongodb:27017); the host + # mapping is just for local mongosh/inspection and must not be exposed off-host (Mongo has no auth). ports: - - "${MONGO_PORT:-27018}:27017" + - "127.0.0.1:${MONGO_PORT:-27018}:27017" networks: [aa-stack] profiles: ["state", "all"] healthcheck: @@ -71,9 +73,12 @@ services: volumes: - aa-data:/data - ./wormdb.json:/etc/wormdb.json:ro + # Loopback-only. 6390 = HTTP read API (curl from the host); 6389 = WormWire (privileged exec/peer + # wire) — the Phase-2 SHiP feed reaches it over the bridge, so neither needs off-host exposure. + # wormdb.json sets require_auth:false (trusted-LOCAL proving deploy); loopback binding is the guard. ports: - - "${WORMDB_GATEWAY_PORT:-6390}:6390" - - "${WORMDB_WIRE_PORT:-6389}:6389" + - "127.0.0.1:${WORMDB_GATEWAY_PORT:-6390}:6390" + - "127.0.0.1:${WORMDB_WIRE_PORT:-6389}:6389" extra_hosts: *host_gw depends_on: aa-loader: From 89d6c2a136e5d824d734c3760f10d038c623254e Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 17:22:34 -0300 Subject: [PATCH 3/7] feat(wseg-build): emit the AtomicAssets config singleton (table 22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aa-build now streams atomicassets-config and writes a sentinel-keyed config record into a new .wseg table (TABLE_AA_CONFIG=22): encode_config/decode_config carry contract, version, collection_format (reusing the schema-format blob), and supported_tokens [(token_contract, symbol, precision)]. AtomicBuilder gains push_config + a finish() emit; the matching Zig decoder + /atomicassets/v1/config route land in wormdb-domain-atomicassets. (version is absent from the snapshot's config table — sourced from the tokenconfigs delta — so it is stored empty for now; a snapshot-load follow-up can fill it.) cargo test -p wseg-build green (config_round_trips, table_ids_are_unique incl. 22). --- crates/wseg-build/src/aa_binfmt.rs | 72 +++++++++++++++++++++++++++ crates/wseg-build/src/aa_builder.rs | 55 +++++++++++++++++++- crates/wseg-build/src/aa_tables.rs | 5 ++ crates/wseg-build/src/bin/aa_build.rs | 4 +- 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/crates/wseg-build/src/aa_binfmt.rs b/crates/wseg-build/src/aa_binfmt.rs index 0992816..11fb047 100644 --- a/crates/wseg-build/src/aa_binfmt.rs +++ b/crates/wseg-build/src/aa_binfmt.rs @@ -377,6 +377,63 @@ pub fn decode_template(b: &[u8]) -> (i32, u64, Vec) { (template_id, schema, immutable) } +// ── config singleton (TABLE_AA_CONFIG) ───────────────────────────────────────────────────────────── +/// Encode the AtomicAssets config singleton. `supported_tokens` = (token_contract name, symbol, +/// precision). Layout: `version | contract(u64) | u16 ver_len | ver | u16 fmt_len | +/// | u16 n_tokens | (u64 token_contract, u8 sym_len, sym, u8 precision) × n`. +pub fn encode_config( + contract: u64, + version: &str, + collection_format: &[(String, String)], + supported_tokens: &[(u64, String, i64)], +) -> Vec { + let mut o = Vec::new(); + o.push(ASSET_VERSION); + pu64(&mut o, contract); + let vb = version.as_bytes(); + let vlen = vb.len().min(u16::MAX as usize); + pu16(&mut o, vlen as u16); + o.extend_from_slice(&vb[..vlen]); + let fmt = encode_schema_format(collection_format); + pu16(&mut o, fmt.len().min(u16::MAX as usize) as u16); + o.extend_from_slice(&fmt); + pu16(&mut o, supported_tokens.len() as u16); + for (tc, sym, prec) in supported_tokens { + pu64(&mut o, *tc); + let sb = sym.as_bytes(); + let sl = sb.len().min(255); + o.push(sl as u8); + o.extend_from_slice(&sb[..sl]); + o.push(*prec as u8); + } + o +} + +/// Decode the config singleton: `(contract, version, collection_format, supported_tokens)`. +pub fn decode_config(b: &[u8]) -> (u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>) { + let mut p = 1usize; + let contract = gu64(b, &mut p); + let vlen = gu16(b, &mut p) as usize; + let version = String::from_utf8_lossy(&b[p..p + vlen]).into_owned(); + p += vlen; + let fmt_len = gu16(b, &mut p) as usize; + let collection_format = decode_schema_format(&b[p..p + fmt_len]); + p += fmt_len; + let n = gu16(b, &mut p) as usize; + let mut tokens = Vec::with_capacity(n); + for _ in 0..n { + let tc = gu64(b, &mut p); + let sl = b[p] as usize; + p += 1; + let sym = String::from_utf8_lossy(&b[p..p + sl]).into_owned(); + p += sl; + let prec = b[p]; + p += 1; + tokens.push((tc, sym, prec)); + } + (contract, version, collection_format, tokens) +} + #[cfg(test)] mod tests { use super::*; @@ -506,4 +563,19 @@ mod tests { assert_eq!(schema, crate::name::encode("pokemon")); assert_eq!(attrs, immutable); } + + #[test] + fn config_round_trips() { + let fmt = vec![ + ("name".to_string(), "string".to_string()), + ("img".to_string(), "ipfs".to_string()), + ]; + let tokens = vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4i64)]; + let blob = encode_config(crate::name::encode("atomicassets"), "1.2.0", &fmt, &tokens); + let (c, v, f, t) = decode_config(&blob); + assert_eq!(c, crate::name::encode("atomicassets")); + assert_eq!(v, "1.2.0"); + assert_eq!(f, fmt); + assert_eq!(t, vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4u8)]); + } } diff --git a/crates/wseg-build/src/aa_builder.rs b/crates/wseg-build/src/aa_builder.rs index 4093a84..23d0380 100644 --- a/crates/wseg-build/src/aa_builder.rs +++ b/crates/wseg-build/src/aa_builder.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use mongodb::bson::{Bson, Document}; use crate::aa_binfmt::{ - encode_asset, encode_posting_hybrid, encode_schema_format, encode_template, Attr, + encode_asset, encode_config, encode_posting_hybrid, encode_schema_format, encode_template, Attr, }; use crate::aa_tables::*; use crate::name; @@ -67,6 +67,8 @@ pub struct AtomicBuilder { all_ids: Vec, /// Data-attribute fields to inverted-index (low-cardinality facets; default `["rarity"]`). data_fields: Vec, + /// The config singleton blob (`/v1/config`), set by the `atomicassets-config` doc. + config: Option>, stats: AaStats, } @@ -90,6 +92,7 @@ impl AtomicBuilder { by_data: HashMap::new(), all_ids: Vec::new(), data_fields, + config: None, stats: AaStats::default(), } } @@ -97,6 +100,7 @@ impl AtomicBuilder { /// Dispatch one Mongo doc by collection. Unknown collections are ignored. pub fn push(&mut self, coll: &str, d: &Document) { match coll { + "atomicassets-config" => self.push_config(d), "atomicassets-schemas" => self.push_schema(d), "atomicassets-templates" => self.push_template(d), "atomicassets-assets" => self.push_asset(d), @@ -104,6 +108,34 @@ impl AtomicBuilder { } } + /// `atomicassets-config` (singleton) → the config blob served at `/v1/config`. + fn push_config(&mut self, d: &Document) { + let contract = doc_str(d, "contract").map(name::encode).unwrap_or(0); + let version = doc_str(d, "version").unwrap_or("").to_string(); + let mut fmt: Vec<(String, String)> = Vec::new(); + if let Ok(arr) = d.get_array("collection_format") { + for f in arr { + if let Bson::Document(fd) = f { + if let (Some(n), Some(t)) = (doc_str(fd, "name"), doc_str(fd, "type")) { + fmt.push((n.to_string(), t.to_string())); + } + } + } + } + let mut tokens: Vec<(u64, String, i64)> = Vec::new(); + if let Ok(arr) = d.get_array("supported_tokens") { + for t in arr { + if let Bson::Document(td) = t { + let tc = doc_str(td, "token_contract").map(name::encode).unwrap_or(0); + let sym = doc_str(td, "token_symbol").unwrap_or("").to_string(); + let prec = doc_i64(td, "token_precision").unwrap_or(0); + tokens.push((tc, sym, prec)); + } + } + } + self.config = Some(encode_config(contract, &version, &fmt, &tokens)); + } + fn push_schema(&mut self, d: &Document) { let (Some(coll), Some(sch)) = (doc_str(d, "collection_name"), doc_str(d, "schema_name")) else { @@ -355,6 +387,19 @@ impl AtomicBuilder { }); tables.push(tmpl_sorted); + // config singleton (one sentinel-keyed entry), if the `atomicassets-config` doc was seen. + if let Some(cfg) = self.config.take() { + tables.push(Table { + table_id: TABLE_AA_CONFIG, + index: vec![IndexEntry { + key: SENTINEL_KEY, + off: 0, + len: cfg.len() as u32, + }], + arena: cfg, + }); + } + self.stats.bytes = tables.iter().map(|t| t.arena.len() as u64).sum(); write_segment(out, tables)?; Ok(self.stats) @@ -402,6 +447,14 @@ fn doc_u32(d: &Document, k: &str) -> u32 { _ => 0, } } +fn doc_i64(d: &Document, k: &str) -> Option { + match d.get(k) { + Some(Bson::Int32(i)) => Some(*i as i64), + Some(Bson::Int64(i)) => Some(*i), + Some(Bson::Double(f)) => Some(*f as i64), + _ => None, + } +} /// Canonical string form of a bson value, matching what the API filters on. fn bson_canon(b: &Bson) -> String { diff --git a/crates/wseg-build/src/aa_tables.rs b/crates/wseg-build/src/aa_tables.rs index 458c5d7..196c449 100644 --- a/crates/wseg-build/src/aa_tables.rs +++ b/crates/wseg-build/src/aa_tables.rs @@ -38,6 +38,9 @@ pub const TABLE_AA_COLL_FWD: u32 = 20; /// by `(template_mint, asset_id)` — the materialized "sort by mint" ordering, reconstructed from the /// snapshot (rank within each template) so a history-looking sort stays sub-µs (no Elasticsearch). pub const TABLE_AA_SORTED_TMPL: u32 = 21; +/// Config singleton: sentinel key [`SENTINEL_KEY`], blob = contract / version / collection_format / +/// supported_tokens (the `/atomicassets/v1/config` payload). +pub const TABLE_AA_CONFIG: u32 = 22; /// The single-entry key used by presorted-ordering tables (one blob, looked up by a fixed key). pub const SENTINEL_KEY: u64 = 0; @@ -159,6 +162,8 @@ mod tests { TABLE_AA_SORTED_ID, TABLE_AA_TMPL_FWD, TABLE_AA_COLL_FWD, + TABLE_AA_SORTED_TMPL, + TABLE_AA_CONFIG, ]; let mut seen = std::collections::HashSet::new(); for id in ids { diff --git a/crates/wseg-build/src/bin/aa_build.rs b/crates/wseg-build/src/bin/aa_build.rs index 83f9db6..26c0f71 100644 --- a/crates/wseg-build/src/bin/aa_build.rs +++ b/crates/wseg-build/src/bin/aa_build.rs @@ -90,7 +90,9 @@ async fn main() -> Result<()> { let t0 = Instant::now(); let mut b = AtomicBuilder::new(fields); - // schemas first (full doc — need `format`), then templates, then assets (projected). + // config first (singleton — contract/collection_format/supported_tokens), then schemas (need + // `format`), then templates, then assets (projected). + stream(&db, "atomicassets-config", doc! {}, 0, &mut b, t0).await?; stream(&db, "atomicassets-schemas", doc! {}, 0, &mut b, t0).await?; stream( &db, From 77c71e511bed401915a64ef7196acfb6f987a68b Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:05:40 -0300 Subject: [PATCH 4/7] fix(wseg-build,deploy): config encode/decode safety + build hardening (PR review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses gemini/copilot review on #8: - aa_binfmt encode_config: cap the length prefix AND the appended bytes together (collection_format blob + supported_tokens iteration) so the header can never desync the body. (gemini HIGH / copilot) - aa_binfmt decode_config: fully bounds-checked, returns Option — a truncated/corrupt blob yields None instead of panicking (DoS). Test updated + malformed-input cases added. (gemini SECURITY-HIGH) - aa_builder: the tiny-segment test now also pushes an atomicassets-config doc, exercising push_config → finish() emit. (copilot) - build-wormdb.sh: stop hiding apt-get errors (-qq stays); SHA-256-pin the Zig tarball + commit-pin meshguard, for reproducible, verified builds. (gemini MED / copilot supply-chain) - entrypoint-loader.sh: deterministic snapshot selection (sort + lexically-newest, warn on multiple) instead of `find | head -1`. (copilot) Deferred (tracked): aa_live::compact_with does not yet carry the config table into a compacted segment — a follow-up for the live-serving daemon path (the frozen-segment Docker stack is unaffected). cargo test -p wseg-build green (27). --- crates/wseg-build/src/aa_binfmt.rs | 44 ++++++++++++++++++++---- crates/wseg-build/src/aa_builder.rs | 7 ++++ deploy/atomicassets/build-wormdb.sh | 15 +++++--- deploy/atomicassets/entrypoint-loader.sh | 14 +++++--- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/crates/wseg-build/src/aa_binfmt.rs b/crates/wseg-build/src/aa_binfmt.rs index 11fb047..7701da5 100644 --- a/crates/wseg-build/src/aa_binfmt.rs +++ b/crates/wseg-build/src/aa_binfmt.rs @@ -395,10 +395,13 @@ pub fn encode_config( pu16(&mut o, vlen as u16); o.extend_from_slice(&vb[..vlen]); let fmt = encode_schema_format(collection_format); - pu16(&mut o, fmt.len().min(u16::MAX as usize) as u16); - o.extend_from_slice(&fmt); - pu16(&mut o, supported_tokens.len() as u16); - for (tc, sym, prec) in supported_tokens { + // Cap the length prefix AND the appended bytes together so the header can never desync the body. + let flen = fmt.len().min(u16::MAX as usize); + pu16(&mut o, flen as u16); + o.extend_from_slice(&fmt[..flen]); + let ntok = supported_tokens.len().min(u16::MAX as usize); + pu16(&mut o, ntok as u16); + for (tc, sym, prec) in supported_tokens.iter().take(ntok) { pu64(&mut o, *tc); let sb = sym.as_bytes(); let sl = sb.len().min(255); @@ -410,28 +413,50 @@ pub fn encode_config( } /// Decode the config singleton: `(contract, version, collection_format, supported_tokens)`. -pub fn decode_config(b: &[u8]) -> (u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>) { +/// Fully bounds-checked — a truncated/corrupt blob returns `None` instead of panicking. +pub fn decode_config(b: &[u8]) -> Option<(u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>)> { + if b.is_empty() { + return None; + } let mut p = 1usize; + if b.len() < p + 8 { + return None; + } let contract = gu64(b, &mut p); + if b.len() < p + 2 { + return None; + } let vlen = gu16(b, &mut p) as usize; + if b.len() < p + vlen + 2 { + return None; + } let version = String::from_utf8_lossy(&b[p..p + vlen]).into_owned(); p += vlen; let fmt_len = gu16(b, &mut p) as usize; + if b.len() < p + fmt_len + 2 { + return None; + } let collection_format = decode_schema_format(&b[p..p + fmt_len]); p += fmt_len; let n = gu16(b, &mut p) as usize; let mut tokens = Vec::with_capacity(n); for _ in 0..n { + if b.len() < p + 9 { + return None; + } let tc = gu64(b, &mut p); let sl = b[p] as usize; p += 1; + if b.len() < p + sl + 1 { + return None; + } let sym = String::from_utf8_lossy(&b[p..p + sl]).into_owned(); p += sl; let prec = b[p]; p += 1; tokens.push((tc, sym, prec)); } - (contract, version, collection_format, tokens) + Some((contract, version, collection_format, tokens)) } #[cfg(test)] @@ -572,10 +597,15 @@ mod tests { ]; let tokens = vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4i64)]; let blob = encode_config(crate::name::encode("atomicassets"), "1.2.0", &fmt, &tokens); - let (c, v, f, t) = decode_config(&blob); + let (c, v, f, t) = decode_config(&blob).expect("decode config"); assert_eq!(c, crate::name::encode("atomicassets")); assert_eq!(v, "1.2.0"); assert_eq!(f, fmt); assert_eq!(t, vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4u8)]); + + // truncated/empty blobs return None instead of panicking (bounds-checked decoder). + assert!(decode_config(&[]).is_none()); + assert!(decode_config(&blob[..blob.len() - 1]).is_none()); + assert!(decode_config(&blob[..3]).is_none()); } } diff --git a/crates/wseg-build/src/aa_builder.rs b/crates/wseg-build/src/aa_builder.rs index 23d0380..b2967e8 100644 --- a/crates/wseg-build/src/aa_builder.rs +++ b/crates/wseg-build/src/aa_builder.rs @@ -481,6 +481,13 @@ mod tests { #[test] fn builds_a_tiny_segment() { let mut b = AtomicBuilder::new(vec!["rarity".to_string()]); + // config singleton flows through the builder (push_config → finish() emit) without panicking. + b.push( + "atomicassets-config", + &doc! { "contract": "atomicassets", + "collection_format": [ {"name":"name","type":"string"} ], + "supported_tokens": [ {"token_contract":"eosio.token","token_symbol":"WAX","token_precision":8i32} ] }, + ); b.push( "atomicassets-schemas", &doc! { "collection_name": "col", "schema_name": "sch", diff --git a/deploy/atomicassets/build-wormdb.sh b/deploy/atomicassets/build-wormdb.sh index 9d42933..3ce604c 100644 --- a/deploy/atomicassets/build-wormdb.sh +++ b/deploy/atomicassets/build-wormdb.sh @@ -28,9 +28,13 @@ set -e ZIG_VER=0.16.0 echo "=== tools ===" apt-get update -qq -apt-get install -y -qq curl xz-utils git build-essential cmake libsodium-dev ca-certificates pkg-config >/dev/null 2>&1 -echo "=== zig ${ZIG_VER} ===" -cd /opt && curl -fsSL "https://ziglang.org/download/${ZIG_VER}/zig-x86_64-linux-${ZIG_VER}.tar.xz" -o zig.tar.xz && tar xf zig.tar.xz +apt-get install -y -qq curl xz-utils git build-essential cmake libsodium-dev ca-certificates pkg-config +echo "=== zig ${ZIG_VER} (sha256-pinned) ===" +# Pinned SHA-256 of zig-x86_64-linux-0.16.0.tar.xz (ziglang.org/download/index.json). Bump in lockstep with ZIG_VER. +ZIG_SHA=70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00 +cd /opt && curl -fsSL "https://ziglang.org/download/${ZIG_VER}/zig-x86_64-linux-${ZIG_VER}.tar.xz" -o zig.tar.xz +echo "${ZIG_SHA} zig.tar.xz" | sha256sum -c - +tar xf zig.tar.xz export PATH="/opt/zig-x86_64-linux-${ZIG_VER}:${PATH}" zig version echo "=== copy 4 repos (sibling layout, excl caches/.git/node_modules) ===" @@ -39,9 +43,12 @@ for r in wormdb wormdb-server wormdb-domain-lightapi wormdb-domain-atomicassets; mkdir -p "/work/$r" tar -C "/host/$r" --exclude=.zig-cache --exclude=zig-out --exclude=.git --exclude=node_modules -cf - . | tar -C "/work/$r" -xf - done -echo "=== meshguard from github (replaces the windows symlink) ===" +echo "=== meshguard from github (replaces the windows symlink; commit-pinned) ===" +# Pinned meshguard commit — keeps the produced wormdb binary reproducible (bump deliberately). +MESHGUARD_SHA=56d9d8d44fbf6256632263e5021caa0f1575f54b rm -rf /work/wormdb/deps/meshguard git clone -q https://github.com/igorls/meshguard /work/wormdb/deps/meshguard +git -C /work/wormdb/deps/meshguard checkout -q "$MESHGUARD_SHA" echo "=== zig build (quic=false) ===" cd /work/wormdb-server zig build --cache-dir /tmp/zc --global-cache-dir /tmp/zgc diff --git a/deploy/atomicassets/entrypoint-loader.sh b/deploy/atomicassets/entrypoint-loader.sh index 9d4b62c..2bc41ea 100644 --- a/deploy/atomicassets/entrypoint-loader.sh +++ b/deploy/atomicassets/entrypoint-loader.sh @@ -12,10 +12,16 @@ AA_DATA_FIELDS="${AA_DATA_FIELDS:-rarity}" ASSET_LIMIT="${ASSET_LIMIT:-0}" # Prefer the EOS-Nation-named portable snapshot (its trailing digits are the block_num that -# snapshot-load auto-derives); fall back to any non-archive .bin. -BIN="$(find /snap -maxdepth 1 -name 'snapshot-*.bin' | head -1 || true)" -[[ -n "$BIN" ]] || BIN="$(find /snap -maxdepth 1 -name '*.bin' ! -name '*archive*' | head -1 || true)" -[[ -n "$BIN" ]] || { echo "[loader] ERROR: no snapshot .bin in /snap (mount SNAPSHOT_DIR)"; ls -la /snap; exit 1; } +# snapshot-load auto-derives); fall back to any non-archive .bin. Selection is DETERMINISTIC: sort the +# matches and take the lexically-newest (the date/height is in the name), warning if there's more than one. +mapfile -t BINS < <(find /snap -maxdepth 1 -name 'snapshot-*.bin' | LC_ALL=C sort) +[[ ${#BINS[@]} -gt 0 ]] || mapfile -t BINS < <(find /snap -maxdepth 1 -name '*.bin' ! -name '*archive*' | LC_ALL=C sort) +[[ ${#BINS[@]} -gt 0 ]] || { echo "[loader] ERROR: no snapshot .bin in /snap (mount SNAPSHOT_DIR)"; ls -la /snap; exit 1; } +if [[ ${#BINS[@]} -gt 1 ]]; then + echo "[loader] WARN: ${#BINS[@]} snapshots in /snap — picking the lexically-newest:" + printf '[loader] %s\n' "${BINS[@]}" +fi +BIN="${BINS[-1]}" echo "[loader] snapshot=$BIN chain=$CHAIN -> $MONGO_URI db=$MONGO_DB" From 291ce1923f1bc7948f409c5f8671a7baaf3221e1 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:36:46 -0300 Subject: [PATCH 5/7] =?UTF-8?q?style(wseg-build):=20cargo=20fmt=20?= =?UTF-8?q?=E2=80=94=20fix=20CI=20fmt=20--check=20(PR=20#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/wseg-build/src/aa_binfmt.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/wseg-build/src/aa_binfmt.rs b/crates/wseg-build/src/aa_binfmt.rs index 7701da5..7a53f57 100644 --- a/crates/wseg-build/src/aa_binfmt.rs +++ b/crates/wseg-build/src/aa_binfmt.rs @@ -414,7 +414,9 @@ pub fn encode_config( /// Decode the config singleton: `(contract, version, collection_format, supported_tokens)`. /// Fully bounds-checked — a truncated/corrupt blob returns `None` instead of panicking. -pub fn decode_config(b: &[u8]) -> Option<(u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>)> { +pub fn decode_config( + b: &[u8], +) -> Option<(u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>)> { if b.is_empty() { return None; } @@ -601,7 +603,10 @@ mod tests { assert_eq!(c, crate::name::encode("atomicassets")); assert_eq!(v, "1.2.0"); assert_eq!(f, fmt); - assert_eq!(t, vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4u8)]); + assert_eq!( + t, + vec![(crate::name::encode("eosio.token"), "EOS".to_string(), 4u8)] + ); // truncated/empty blobs return None instead of panicking (bounds-checked decoder). assert!(decode_config(&[]).is_none()); From df72934abf2ed901cbe5b23b93a7f0fda12d0705 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:39:37 -0300 Subject: [PATCH 6/7] fix(wseg-build): factor decode_config return into a type alias (clippy type_complexity, CI #8) --- crates/wseg-build/src/aa_binfmt.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/wseg-build/src/aa_binfmt.rs b/crates/wseg-build/src/aa_binfmt.rs index 7a53f57..e46e780 100644 --- a/crates/wseg-build/src/aa_binfmt.rs +++ b/crates/wseg-build/src/aa_binfmt.rs @@ -412,11 +412,13 @@ pub fn encode_config( o } -/// Decode the config singleton: `(contract, version, collection_format, supported_tokens)`. -/// Fully bounds-checked — a truncated/corrupt blob returns `None` instead of panicking. -pub fn decode_config( - b: &[u8], -) -> Option<(u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>)> { +/// Decoded config singleton: `(contract, version, collection_format, supported_tokens)`, +/// where each supported token is `(token_contract, symbol, precision)`. +pub type DecodedConfig = (u64, String, Vec<(String, String)>, Vec<(u64, String, u8)>); + +/// Decode the config singleton. Fully bounds-checked — a truncated/corrupt blob returns `None` +/// instead of panicking. +pub fn decode_config(b: &[u8]) -> Option { if b.is_empty() { return None; } From 653c71ad982a02615b0d55e90142def747d513d2 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:04:43 -0300 Subject: [PATCH 7/7] feat(snapshot-load): source atomicassets config `version` from tokenconfigs (PR review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /atomicassets/v1/config was serving version:"" because map_config never captured it — the AtomicAssets `version` lives in the contract's `tokenconfigs` table ({standard, version}), not the `config` row. The schema-registry pre-pass now also reads `atomicassets:tokenconfigs` and stashes `version` on SchemaRegistry; map_config emits it when present and OMITS the field entirely on pre-v2 chains that lack tokenconfigs (rather than encoding an invalid ""). Validated on Jungle 4: tokenconfigs.version = "1.3.1" → the Mongo config doc + the served /config now report "1.3.1". cargo check/clippy/fmt green. --- crates/snapshot-load/src/atomicassets.rs | 17 ++++++++++++++++- crates/snapshot-load/src/map.rs | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/snapshot-load/src/atomicassets.rs b/crates/snapshot-load/src/atomicassets.rs index b900282..5d6c0a7 100644 --- a/crates/snapshot-load/src/atomicassets.rs +++ b/crates/snapshot-load/src/atomicassets.rs @@ -75,6 +75,9 @@ pub fn all_collections() -> Vec<&'static str> { pub struct SchemaRegistry { schemas: HashMap>>, collection_format: Vec, + /// The contract `version` from the `tokenconfigs` table (v2). Empty on pre-v2 chains that lack it + /// — `map_config` then omits the field rather than emitting an invalid `""`. + version: String, } impl SchemaRegistry { @@ -114,6 +117,7 @@ pub fn build_schema_registry( let aa = nm("atomicassets")?; let schemas_t = nm("schemas")?; let config_t = nm("config")?; + let tokenconfigs_t = nm("tokenconfigs")?; let mut reg = SchemaRegistry::default(); @@ -131,6 +135,7 @@ pub fn build_schema_registry( filters: vec![ Filter::CodeTable(aa, schemas_t), Filter::CodeTable(aa, config_t), + Filter::CodeTable(aa, tokenconfigs_t), ], }; @@ -165,6 +170,11 @@ pub fn build_schema_registry( ) { reg.collection_format = fields; } + } else if row.table == tokenconfigs_t { + // tokenconfigs (v2) → the contract `version` the API's /config reports. + if let Some(v) = data.get("version").and_then(Value::as_str) { + reg.version = v.to_string(); + } } Ok(()) }; @@ -339,9 +349,14 @@ pub fn map_offer(ctx: &RowCtx, data: Value) -> Value { /// `config` singleton → config doc. Counters are live on-chain (S); `supported_tokens` is flattened to /// `{token_contract, token_symbol, token_precision}`. -pub fn map_config(ctx: &RowCtx, data: Value) -> Value { +pub fn map_config(ctx: &RowCtx, data: Value, reg: &SchemaRegistry) -> Value { let mut doc = Map::new(); doc.insert("contract".into(), json!(ctx.code)); + // `version` comes from the `tokenconfigs` table (read in the pre-pass), NOT the `config` row. + // Omit it entirely when the chain has no tokenconfigs (pre-v2) rather than emit an invalid "". + if !reg.version.is_empty() { + doc.insert("version".into(), json!(reg.version)); + } for f in [ "asset_counter", "template_counter", diff --git a/crates/snapshot-load/src/map.rs b/crates/snapshot-load/src/map.rs index ddbe168..c9e1f5e 100644 --- a/crates/snapshot-load/src/map.rs +++ b/crates/snapshot-load/src/map.rs @@ -84,7 +84,7 @@ pub fn map_row( )), ("atomicassets", "config") => Some(( atomicassets::COLL_AA_CONFIG, - atomicassets::map_config(ctx, data), + atomicassets::map_config(ctx, data, schema_reg), )), // AtomicMarket state (the `atomicmarket`/`atomic` preset). ("atomicmarket", "sales") => Some((