Commit 8dd4e7e
fix(token): preserve absolute lifecycle.end across mint round-trip (#110)
* fix(token): preserve absolute lifecycle.end across mint round-trip
The packed token stores `[minted_at, start_delay, end_delay]` where both
delays are non-negative u32s, so `lifecycle.start >= minted_at` is a
structural invariant — there is no representation for a window that
opened before mint time. Previously the mint logic clamped `start_delay`
to 0 silently when `start <= current_time`, but still computed
`end_delay = lifecycle.end - lifecycle.start` from the *unclamped* start.
The reconstructed end therefore landed at `mint_time + (end - start)`
rather than the caller's intended `lifecycle.end`, silently extending
the token's playable window past the cutoff the caller specified.
Two cases that hit this:
1. Late entry into a window that already opened (e.g. a no-registration
tournament where a player enters during the live phase): caller passes
`start = absolute_game_start, end = absolute_game_end`, expects token
playable through `game_end`, gets a token playable through
`mint_time + game_duration` instead.
2. Caller passes `start = None` (defaults to 0) with an absolute `end`:
`end_delay = end - 0 = end`, which both overflows the 25-bit field
for any realistic timestamp and encodes the wrong window.
Fix: clamp `effective_start` to `max(start, current_time)` and derive
`end_delay` from the clamped value. The token's playable window for late
entries becomes `[mint_time, end]` — which matches reality, since the
token didn't exist during the part of the window that already passed.
Also panic when a non-zero `end` is at or before `current_time`. Without
this, `end_delay` collapses to 0 — which the storage format treats as
"no expiration" — silently producing a token that lives forever instead
of one that's already expired (almost certainly a caller bug).
Updated `test_core_token_playability_with_lifecycle` to use absolute
`end` semantics; the previous version passed `end = PAST_TIME + 1` and
relied on the buggy `end - start` duration encoding to make the token
expire one second after mint. Added round-trip tests covering both
\`start in past\` and \`start unset\` cases.
* fix(token): also reject end == start; cover panic path with should_panic tests
Strengthen the lifecycle precondition added in 7c1835a: a non-zero `end`
must be strictly greater than `lifecycle.start` as well as `current_time`.
`validate()` only enforces `start <= end`, so `start == end > current_time`
slipped through the original check, then fell into the `end_delay = 0`
branch downstream — silently producing exactly the immortal-token failure
mode this PR was supposed to eliminate, just on a different path. A
zero-length window can never be playable (`is_playable` requires
`start <= now < end`), so callers passing `start == end` are buggy.
Cover the assertion's panic branch with three `#[should_panic]` regression
tests (closing the codecov gap and matching the Codex/CodeRabbit asks):
- `end < current_time`
- `end == current_time`
- `start == end > current_time` (zero-length window)
* test(token): cover remaining mint lifecycle edge cases
Adds regression tests for the cases that the existing PR didn't
exercise directly: explicit Some(0)/Some(0) (no-expiration), gated
start with no end, equal start/end in the past, both start and end
in the past with explicit start, and a window exceeding the 25-bit
end_delay pack limit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 90121ff commit 8dd4e7e
3 files changed
Lines changed: 431 additions & 14 deletions
File tree
- packages/embeddable_game_standard/src/token
- tests
Lines changed: 6 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
600 | 600 | | |
601 | 601 | | |
602 | 602 | | |
603 | | - | |
604 | | - | |
605 | | - | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
606 | 606 | | |
607 | 607 | | |
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
611 | 611 | | |
612 | 612 | | |
613 | | - | |
| 613 | + | |
614 | 614 | | |
615 | 615 | | |
616 | 616 | | |
| |||
646 | 646 | | |
647 | 647 | | |
648 | 648 | | |
649 | | - | |
650 | | - | |
651 | | - | |
652 | | - | |
| 649 | + | |
| 650 | + | |
653 | 651 | | |
654 | 652 | | |
655 | 653 | | |
| |||
0 commit comments