Skip to content

Build The Arkiv Beacon Challenge End to End

This guide walks through the complete Arkiv Beacon implementation. Follow it as you maintain the existing challenge or adapt it when cloning the structure for future slugs: every step—from template copy to solver verification—is represented here.


Step 1. Start With the Template

  1. Copy the scaffold:

bash cp -r challenges/_template challenges/arkiv-beacon

  1. Update metadata:
  2. Edit challenges/arkiv-beacon/README.md, ANNOUNCEMENT.md, and Dockerfile labels with the proper slug and title.
  3. Describe the registration-first flow, hint structure, and final objective.
  4. Install server dependencies so you can run FastAPI locally:

bash python -m pip install -r challenges/arkiv-beacon/server/requirements.txt

  1. Clone the helper repositories next to this project so editors and AI tools can inspect them offline:

bash git clone https://github.com/Arkiv-Network/arkiv-sdk-python git clone https://github.com/Golem-Base/golembase-op-geth

Keeping these directories adjacent to ctf-ideas/ ensures IDEs can jump into the SDK and op-geth sources while you build.


Step 2. Bring Up FastAPI and the Arkiv SDK

Inside server/app.py we import the SDK types and create the FastAPI application:

from fastapi import FastAPI
app = FastAPI(title="Arkiv CTF - Arkiv Beacon", version="1.0.0")

The server exposes only /healthz. All player actions happen on-chain via Arkiv entities, so HTTP stays simple.


Step 3. Configure Logging and Metrics Once

from ctf.common.observability import configure_logging_and_metrics

metrics = configure_logging_and_metrics(
    ctf_slug="arkiv-beacon",
    service="server",
    logger_name="ctf.arkiv-beacon",
)

The metrics helper seeds Prometheus counters and pushes to Loki/Prometheus when their URLs are configured. Logs follow the shared logfmt style so teams can trace registrations (evt=register.accepted), ping attempts (evt=ping.accepted|rejected), and solves (evt=beacon.online).


Step 4. Maintain Per-Team Beacon State

BeaconInfo (a @dataclass) tracks entity keys, team names, player addresses, ping counts, and the current status. The app.state namespace holds lookup tables:

  • beacons_by_team / beacons_by_id for quick routing.
  • team_registered_at and team_attempts for analytics.
  • webhook_sent to avoid duplicate notifications.

Keeping runtime state in one place makes it trivial to reset between tests and lets the solver read predictable annotations.


Step 5. React to Registrations (No Polling)

During startup the server calls ArkivClient.watch_logs(...) with callbacks for create/update/delete/extend events. _handle_registration listens for entities annotated with register_for="arkiv-beacon" and team="<name>".

For every registration:

  1. Record the registering wallet address and timestamp.
  2. Create a team-specific beacon entity annotated with ctf="arkiv-beacon", team, and beacon_for="<registration_entity>".
  3. Store the new entity in the lookup tables and log a human-readable message:
Team registered team=alpha addr=0x1234beef ctf=arkiv-beacon evt=register.accepted outcome=accepted

Initial payload:

{
  "status": "Dark",
  "ping_count": 0,
  "last_message": null,
  "hint": "Three pings light the beacon.",
  "updated_at": "<iso8601>"
}

Step 6. Process Ping Entities

Players submit pings by creating entities annotated with ping_for_beacon="<beacon_entity>" and team="<name>". _handle_ping enforces:

  • The referenced beacon exists and belongs to the submitting team.
  • The payload contains a non-empty message string.
  • Duplicate attempts after a beacon is online are ignored.

Each accepted ping increments ping_count, updates the beacon payload via ArkivUpdate, and refreshes the hint. The third ping must echo the activation phrase revealed by the server (let there be light). When that happens the payload becomes:

{
  "status": "Online!",
  "ping_count": 3,
  "last_message": "let there be light",
  "flag": "ARKIV_BEACON_BEGINNER_FLAG",
  "updated_at": "<iso8601>"
}

Representative logs:

Ping accepted team=alpha ping_count=2 ctf=arkiv-beacon evt=ping.accepted entity=0x1234…cdef outcome=accepted
Beacon online team=alpha attempts=3 ctf=arkiv-beacon evt=beacon.online entity=0x1234…cdef outcome=accepted

Metrics mirror these events through ctf_beacon_pings_total and shared record_attempt / record_solve hooks.


Step 7. Announce Completions on Discord

When a beacon turns Online!, _handle_ping schedules send_discord_webhook with asyncio_run_safely. The payload includes challenge, team, address, timestamp, and result="accepted". Missing DISCORD_WEBHOOK_URL simply skips the notification, which keeps local runs quiet.


Step 8. Startup and Shutdown Lifecycle

  • @app.on_event("startup") validates ARKIV_PRIVATE_KEY, creates the read/write Arkiv client, logs the wallet balance, subscribes to events, and ensures previously created beacons are loaded into memory.
  • @app.on_event("shutdown") unsubscribes from the watch handle and disconnects the client, preventing dangling tasks when Uvicorn stops.

This lifecycle guarantees the server processes every registration and ping even if the container restarts.


Step 9. Validate the Flow with the Solver

challenges/arkiv-beacon/scripts/solve_beacon.py exercises the whole path:

  1. Register (register_for="arkiv-beacon", team="solver-beacon").
  2. Query the provisioned beacon entity.
  3. Send two calibration pings to charge the beacon.
  4. Read the updated hint, extract the activation phrase, and send it as the final ping.
  5. Confirm the beacon reports status="Online!" and print the flag.

Example command:

source .venv/bin/activate
pip install -U pip arkiv-sdk==1.0.0a8 eth-account
export ARKIV_RPC_URL=http://localhost:8545
export ARKIV_WS_URL=ws://localhost:8545
export ARKIV_PRIVATE_KEY=<funded_hex>  # prefix optional
python challenges/arkiv-beacon/scripts/solve_beacon.py

You should see ARKIV_BEACON_BEGINNER_FLAG in stdout and matching server logs for ping.accepted and beacon.online.


Deployment Checklist

  1. Build the container from repo root:

bash docker build -f challenges/arkiv-beacon/server/Dockerfile -t arkiv-ctf:arkiv-beacon .

  1. Run locally:

bash docker run --rm -p 8380:8080 \ -e ARKIV_RPC_URL=http://host.docker.internal:8545 \ -e ARKIV_WS_URL=ws://host.docker.internal:8545 \ -e ARKIV_PRIVATE_KEY=<hex> \ -e ARKIV_BEACON_FLAG=ARKIV_BEACON_BEGINNER_FLAG \ arkiv-ctf:arkiv-beacon

  1. Confirm /healthz responds with "ok" and tail logs for the registration / ping events while running the solver.

  2. Wire the container into infra/docker-compose.yml (already done for Arkiv Beacon) and ensure make e2e passes before shipping changes.

Following this guide keeps the Arkiv Beacon challenge—and any future clone of the pattern—consistent with the repository’s expectations for observability, documentation, and end-to-end validation.