Files
HIPCTF2/docs/database/challenges.md
T

6.9 KiB
Raw Blame History

type, title, description, tags, timestamp
type title description tags timestamp
database Challenge Tables category, challenge, challenge_file, and solve tables — how CTF challenges and scoring are stored.
database
challenge
category
solve
2026-07-22T20:30:00Z

Tables

category

Column Type Description
id TEXT PK UUID v4.
system_key TEXT Non-null for system categories (e.g. CRY, MSC, PWN, REV, WEB, HW); unique where NOT NULL; NULL for user-created categories.
name TEXT Display name (e.g. Cryptography).
abbreviation TEXT Short label (e.g. CRY), 26 chars, server-uppercased. Globally unique — enforced by uq_category_abbreviation.
description TEXT Optional description.
icon_path TEXT Default icon URL (e.g. /uploads/icons/CRY.png).
created_at TEXT ISO 8601 timestamp the row was created.
updated_at TEXT ISO 8601 timestamp the row was last updated.

System rows are seeded by the UpdateSystemCategoryKeys1700000000300 migration so the canonical keys are CRY (Cryptography), MSC (Misc), PWN (Binary exploitation), REV (Reverse Engineering), WEB (Web), and HW (Hardware). The forward-only RepairCategorySchemaAndSystemCategories1700000000400 migration repairs legacy schemas on existing databases.

challenge

Column Type Description
id TEXT PK UUID v4.
name TEXT Display name. Case-insensitive uniqueness enforced by uq_challenge_name_lower.
description_md TEXT Markdown description.
category_id TEXT FK → category.id (ON DELETE RESTRICT).
difficulty TEXT 'LOW' / 'MEDIUM' / 'HIGH'.
initial_points INTEGER Starting point value (1100000).
minimum_points INTEGER Floor after decay (1100000, must be ≤ initial_points).
decay_solves INTEGER Solves at which decay reaches the minimum (110000).
flag TEXT Submission string expected from players. Never returned in non-admin DTOs.
protocol TEXT 'NC' (netcat-style connection) or 'WEB' (browser-based).
port INTEGER TCP port (nullable for WEB, required 165535 for NC).
ip_address TEXT IP the challenge listens on (defaults to setting.defaultChallengeIp).
enabled INTEGER Boolean — 1 (default) or 0.
created_at TEXT ISO 8601 timestamp the row was created.
updated_at TEXT ISO 8601 timestamp the row was last updated (bumped on every PUT).

The UpgradeChallengeAdminSchema1700000000500 migration rebuilt the challenge table atomically (preserving legacy data), introduced canonical enums, the enabled flag, updated_at, the LOWER(name) unique index, and tighter scoring defaults. The category FK stays at ON DELETE RESTRICT so the CATEGORY_HAS_CHALLENGES business rule is preserved.

challenge_file

Column Type Description
id TEXT PK UUID v4.
challenge_id TEXT FK → challenge.id (ON DELETE CASCADE).
original_filename TEXT Filename from upload.
stored_filename TEXT UUID-based filename on disk under UPLOAD_DIR/challenges/; unique.
mime_type TEXT Source MIME type (defaults to application/octet-stream).
size_bytes INTEGER Decoded file size in bytes.
created_at TEXT ISO 8601 timestamp.

The upgrade migration added stored_filename, mime_type, size_bytes, and created_at. The challenge → file FK was changed from RESTRICT to CASCADE so deleting a challenge in one DB transaction removes all its ChallengeFile rows in lockstep.

solve

Column Type Description
id TEXT PK UUID v4.
challenge_id TEXT FK → challenge.id (ON DELETE CASCADE).
user_id TEXT FK → user.id (ON DELETE RESTRICT).
solved_at TEXT ISO 8601 timestamp the solve was recorded.
points_awarded INTEGER Points actually awarded (after decay).
base_points INTEGER Snapshot of challenge.initial_points at solve time.
rank_bonus INTEGER Extra points from rank (e.g. first-blood).
is_first INTEGER Boolean — first-blood flag.
is_second INTEGER Boolean — second-blood flag.
is_third INTEGER Boolean — third-blood flag.

uq_solve_challenge_user on (challenge_id, user_id) is retained; deleting a challenge now cascades to its solve rows, while deleting a user still requires explicit handling (user_id remains RESTRICT).

Scoring formula

points_awarded = clamp(initial_points - decay, minimum_points, initial_points) where decay = max(0, base_points - minimum_points) * solvesBefore / decay_solves, and additional rank_bonus may be added for early solves.

See also