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¶
- Copy the scaffold:
bash
cp -r challenges/_template challenges/arkiv-beacon
- Update metadata:
- Edit
challenges/arkiv-beacon/README.md,ANNOUNCEMENT.md, and Dockerfile labels with the proper slug and title. - Describe the registration-first flow, hint structure, and final objective.
- Install server dependencies so you can run FastAPI locally:
bash
python -m pip install -r challenges/arkiv-beacon/server/requirements.txt
- 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_idfor quick routing.team_registered_atandteam_attemptsfor analytics.webhook_sentto 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:
- Record the registering wallet address and timestamp.
- Create a team-specific beacon entity annotated with
ctf="arkiv-beacon",team, andbeacon_for="<registration_entity>". - Store the new entity in the lookup tables and log a human-readable message:
Team registered team=alpha addr=0x1234…beef 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
messagestring. - 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")validatesARKIV_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:
- Register (
register_for="arkiv-beacon",team="solver-beacon"). - Query the provisioned beacon entity.
- Send two calibration pings to charge the beacon.
- Read the updated hint, extract the activation phrase, and send it as the final ping.
- 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¶
- Build the container from repo root:
bash
docker build -f challenges/arkiv-beacon/server/Dockerfile -t arkiv-ctf:arkiv-beacon .
- 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
-
Confirm
/healthzresponds with"ok"and tail logs for the registration / ping events while running the solver. -
Wire the container into
infra/docker-compose.yml(already done for Arkiv Beacon) and ensuremake e2epasses 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.