Sashité Sanki Session
The session kernel of the Sanki game suite, built for
Sashité. The L2 layer over
sashite-sanki-engine: given a
session's public events, it computes the verdict the rule system yields at a
given instant — and therefore whether a player's Conclusion (kind 3425),
which is binding by correctness, actually terminates the session. Published
under the Apache-2.0 license.
Until 0.12 this crate was sashite-sanki-arbiter: the same replay, run by a
designated arbiter that signed the verdict. Since the suite's 2026-09-04
revision (ADR-0033) there is no arbiter — either player concludes, and every
consumer recomputes. The crate is the reference implementation of the session
part of the sashite.sanki.kernel/1 rule system.
The event model is abstract and carries no Nostr dependency: the same kernel can be driven from any context able to supply the events — a rating authority, a client, a bot, an indexer. Identity, transport, signature verification and cross-event structural validation are the caller's responsibility.
Event model
The kernel reasons over plain values the caller has already received, signature-verified, and parsed — there is no cryptography and no I/O here:
Ply— a move played at a step (the kind-3423 content);Attestation— a timestamper's attestation of an event (kind 3410);Conclusion— a player's verdict on the session (kind 3425): who concludes, when (its canonical timing is the cutoff), and what it claims — aVerdict, a termination status and an outcome, coherent by construction (Verdict::newrefuses a decisive status with a draw, or the converse: a claim the rule system can never yield is refused before any replay);EventId/PublicKey— opaque 32-byte identities (event ids unique, as Nostr guarantees — the kernel tells candidates apart by id).
Timing depends on the session's mode: in attested mode (a designated
timestamper) each event's canonical timing is its attestation's created_at,
the event's own being an informational self-claim; in self-timed mode
(designated timing relays, no timestamper — what Sashité deploys for sanki)
the event's own created_at, as accepted by a designated timing relay, is the
canonical timing. Ply and Conclusion carry their created_at for the
self-timed case — on the precondition that the caller offers the kernel only
events whose acceptance by a designated timing relay is established; an
event without that is pending, not a candidate for anything, and this crate
has no way to tell (it sees no relay).
Evaluation
verdict_at(params, plies, attestations, invoker, cutoff) -> Result<Verdict, NoVerdict>
is the primitive: the verdict the rule system yields at cutoff, with invoker
as the concluding side. expected_verdict(params, plies, attestations, conclusion)
wraps it, resolving the invoker and cutoff from a Conclusion (its signer and its
canonical timing, cutoff_of) — what that Conclusion is expected to claim:
- canonical timing gives each Ply its instant (its attestation's
created_atin attested mode — smallest attestation as tiebreaker — its owncreated_atwhen self-timed) —stepbeing each signer's own move ordinal; - the natural state replays the interleaved play order (within each step
value, side
firstbefore sidesecond), selecting each(session, signer, step)slot's canonical Ply by the two-window forgiving rule against the slot's boundaryT(the maximum canonical timing among the preceding half-moves — never rewound by a selected premove — t₀ for the first slot): among anterior candidates (timed beforeT— premoves) the latest legal wins; failing that, among informed candidates (timed at/afterT— live moves) the earliest legal wins; each within a per-window capK, legality probed lazily on the capped windows only (≤ 2K full-rule probes per slot, the normative anti-flooding bound;Kis the session'scandidate_cap, the rule-system document's, at least 1 by type). A Ply timed before t₀ is invalid and never enters a slot (one timed at t₀ does); identical candidates (same content, samedrawflag) count once against the cap, represented by the one the window's scan reaches first, so the collapse never changes the selection. An illegal candidate — premove or live — is always skipped, never a loss (there is noillegalmove); - the verdict is entirely play-derived (there is no equivocation sanction): a termination reached during replay — a rule-system ending or a played-Ply timeout — otherwise, on a still-ongoing position, the invocation resolved in order: draw acceptance, abandonment timeout, residual resignation (decisive against the concluding player, whatever the turn). Concluding is at the signer's risk: they choose when, never what.
expected_verdict returns Err(NoVerdict) — a typed reason, not a bare None
— when no verdict is defined: OtherSession or NotAPlayer (kind 3425
§Semantic constraints, items 2–3), BeforeStart (a cutoff before t₀ — item 9;
the kernel is not invoked before the session is playable), Pending (no
canonical timing yet — the one transient reason, NoVerdict::is_transient),
or Inconsistent (a broken internal invariant, never a silently wrong
verdict). A consumer must tell the transient case from the final ones: a
pending Conclusion is re-examined once timed, an invalid one is ignored for
good.
Two functions turn the verdict into the suite's protocol rules:
check(params, plies, attestations, conclusion) -> Check— item 8 of the Conclusion's semantic constraints:Conforming(verdict)when the claim equals the verdict the rule system yields,Wrong { claimed, expected }when it differs (a non-conforming Conclusion does not terminate the session, whatever its timing), orNoVerdict(reason).conforms(…) -> boolischeckreduced to "is itConforming";select_conclusion(params, plies, attestations, conclusions) -> Result<Option<CanonicalConclusion>, NoVerdict>— the session's canonical Conclusion (kind 3425 §Idempotence and finality): the earliest conforming one by canonical timing, smallest event id as tiebreaker, returned with its cutoff and verdict so no second replay is needed;Ok(None)while none conforms with established timing. Two conforming Conclusions with different cutoffs may carry different verdicts (a premature claim resolving to its signer's resignation, a later win on time); the earliest rules. An inconsistent replay isErr: the session is unresolved for this consumer, and no other Conclusion is promoted around it.
To decide whether to conclude, a client or bot calls verdict_at with its own
side and the present instant — no synthetic event — and publishes exactly that
verdict; Verdict::scores(params) gives the two result tags to carry.
Design guarantees
- Panic-free by construction. Crate lints forbid
unsafe, and denyunwrap/expect/panic, slice indexing, and overflowing arithmetic. - Deterministic. The result is a pure function of the events; identity, transport, and signature verification are the caller's responsibility.
- In lockstep with the reference module. The vendored conformance corpus
(
tests/conformance/, v10) is the shared test suite of the Sanki kernel — this crate replays it natively, and the reference WebAssembly module built from this crate replays it through its ABI (ADR-0034): the two cannot drift on which Ply is canonical, on how a chain ends, nor — the vectors carrying aninvokerand anexpectedVerdict— on the post-chain verdict. - Reported, never guessed. A Conclusion out of reach gets a typed reason;
a broken internal invariant during replay is
NoVerdict::Inconsistent, not a wrong resignation — andselect_conclusionpropagates it rather than promoting the next Conclusion in line. - Refused at the boundary, not in the replay. What the rule system can
never yield — a self-play
Seats, a founding position withoutfirstto move, a cap of 0, a status and an outcome of different kinds, a split other than 100/0, 50/50, 0/100 — is refused by the constructors (Option, or the type), so the kernel only ever evaluates sessions and claims it has a defined answer for.
Usage
[]
= "0.14"
use ;
use ;
use ;
use Side;
use ;
use ;
use ;
use Position;
// Identities (opaque 32-byte values; the caller maps them from its own source).
let session = from_bytes;
let timestamper = from_bytes;
let first = from_bytes;
let second = from_bytes;
// The session's invariant parameters, including the initial position (FEEN,
// `first` to move — as every position a Sanki rule-system document
// prescribes). A designated timestamper puts the session in attested mode;
// `None` would make it self-timed (each event's own `created_at`, as accepted
// by a designated timing relay, authoritative).
let period = new.expect;
let params = new
.expect;
// One ply: the first player plays Ra1-a8, a back-rank mate. In attested mode
// the ply's own `created_at` (last argument) is an informational self-claim;
// the timestamper's attestation below is authoritative.
let plies = ;
// The second player concludes, claiming checkmate for the first player (a
// coherent claim: a decisive status with a decisive outcome); the timestamper
// has attested both the ply (t=100) and the Conclusion (t=1000, the cutoff).
let conclusion = new;
let attestations = ;
// Checking the Conclusion on the rules axis: the claim it carries equals the
// verdict the rule system yields at its cutoff, so it is Conforming and
// terminates the session.
match check
Built on
sashite-sanki-engine (the rules
engine), which it uses under the full rule system — ōgi's uchifuzume included:
candidate legality is probed via the façade's validate, and the selected Ply
is applied via the kernel's per-ply step. The two agree by construction
(pinned by an exhaustive oracle test across every variant pairing); should
they ever disagree, the replay reports ChainEnd::Inconsistent and no verdict
is derived.
Minimum supported Rust version
Rust 1.81.
License
Licensed under the Apache License, Version 2.0. See NOTICE.