feat(puzzle): add custom puzzle builder validation and tamper-evident replay verification - #455
Merged
Mkalbani merged 1 commit intoSep 28, 2026
Conversation
… replay verification
|
@nafiuishaaq Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds custom puzzle authoring and tamper-evident replay verification to the
puzzle engine service.
What this adds
src/puzzle/puzzle.service.ts:validatePuzzleDefinition()rejects malformed boards with field-levelerrors (
{ field, code, message }) rather than throwing, so the builder UIcan attach each message to the input that caused it. It checks grid bounds,
that
cellsandsolutionboth matchwidth * height, that every value isin
allowedValues, and that neither board nor solution is trivial.estimateDifficulty()returns a score, a band (easy/medium/hard/expert) and the raw inputs it used, so a preview can be shown andrecalibrated later. It is a transparent heuristic — log-scaled board area,
distinct-value variety, and solution run density — not a solver.
submitCustomPuzzle()runs validation and returns a moderation envelope. Avalid puzzle is never published directly: it always enters
pending_reviewwith a
definitionDigestover the canonicalised definition, so a latermoderation decision can be checked against the exact bytes that were
validated.
moderateSubmission()moves a submission toapproved/rejected, refusingto moderate a non-pending submission or one reviewed by its own author.
recordReplayAction()requires contiguous sequence numbers andnon-decreasing timestamps, so a replay cannot skip or reorder moves, and
folds the previous chain hash into each action digest.
finalizeReplay()seals a replay into a summary (action count, duration,hints used, chain hash, submitted-value hash) for playback and sharing.
verifyReplay()checks three things before a result is trusted: the actionsstill hash to the sealed chain digest (
CHAIN_TAMPERED), the submittedvalue matches the recorded solution hash (
SOLUTION_MISMATCH), and the runwas not faster than
MIN_PLAUSIBLE_SOLVE_MS(IMPLAUSIBLE_SOLVE_TIME).createPuzzle()now uses a shared privatehash()helper instead ofinlining
createHash, which the new code also needs.Notes
editing
puzzle.module.ts, a second file, so the builder and replay work isbuilt on the already-injected
SorobanServiceandnode:crypto. As a resultsubmissions and replays are returned as envelopes rather than persisted;
wiring them to storage is the follow-up this change deliberately leaves out.
chain detects edits to a stored replay, but an attacker who can rewrite the
whole record can also rewrite the chain. Sealing the final digest somewhere
append-only (or signing it) is what would actually prevent forgery.
estimateDifficultywas checked against small/medium/large boards whiledeveloping and now yields easy/medium/hard respectively. An earlier version
normalised run density by
area / 2, which let a 2x2 board max out that termand score "medium"; it is normalised by
areanow.will disagree with players until it is calibrated.
inputsis returnedprecisely so that calibration is possible later.
included — they live in
src/replay/andsrc/puzzle/session-replay.service.ts,which are separate files.
Issues
Closes #435
Closes #436
Closes #437
Closes #438