social-kit / src / ardegazu / social / gamelb.cljs
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
;; ported-from: src/gamelb.ts @ v1.2.0
;;
;; The in-game co-sign engine: turns a finished match into a verified,
;; deposited receipt with three frames on the game's own wire:
;;
;;   host -> all   {t:"lbq", base}   the unsigned receipt — please co-sign
;;   player -> host{t:"lbs", p, s}   my signature (only if MY score is right)
;;   host -> all   {t:"lbr", rec}    the finished receipt — store + deposit
;;
;; Only players whose suite identity was verified in the sealed hello appear
;; in the score list; fewer than MIN_SIGNERS identity-ful players => no receipt
;; at all (an unco-signable match just isn't on the board). Every acceptor
;; re-verifies from scratch — the host is a referee, not an authority.
(ns ardegazu.social.gamelb
  (:require [ardegazu.social.receipts :as receipts]
            [shadow.cljs.modern :refer (defclass js-await)]))

(def ^:private SIG-COLLECT-MS 6000)

(defclass GameLb
  (constructor [this opts]
    (unchecked-set this "_o" opts)
    (unchecked-set this "receipts" (receipts/ReceiptSet. (unchecked-get opts "ns")))
    (unchecked-set this "_pending" nil)
    (unchecked-set this "_sigs" (js/Map.))
    (unchecked-set this "_timer" 0)))

(defn- lb-accept [self raw]
  (js-await [v (receipts/verify-receipt raw js/undefined)]
    (when (some? v)
      (js-await [added (.add ^js (unchecked-get self "receipts") (unchecked-get v "rec"))]
        (when added
          ;; server dedups by SHA-256 — redundant deposits are free
          (receipts/deposit-receipt (unchecked-get v "rec"))
          (let [o (unchecked-get self "_o")]
            (when-let [cb (unchecked-get o "onReceipt")]
              (cb (unchecked-get v "rec"))))
          nil)))))

(defn- lb-finalize [self]
  (let [base (unchecked-get self "_pending")]
    (unchecked-set self "_pending" nil)
    (if (nil? base)
      (js/Promise.resolve nil)
      (let [sigs (-> (js/Array.from (.entries ^js (unchecked-get self "_sigs")))
                     (.map (fn [e] (js-obj "p" (aget e 0) "s" (aget e 1)))))
            rec (js/Object.assign (js-obj) base)]
        (unchecked-set rec "sig" sigs)
        (js-await [v (receipts/verify-receipt rec js/undefined)]
          (if (nil? v)
            nil ; not enough valid co-signers — the match stays off-board
            (let [o (unchecked-get self "_o")
                  net (unchecked-get o "net")]
              (.broadcast ^js net (js-obj "t" "lbr" "rec" (unchecked-get v "rec")))
              (lb-accept self (unchecked-get v "rec")))))))))

(defn- lb-co-sign
  "PLAYER: sign the host's base iff my own entry matches my own belief."
  [self from raw-base]
  (let [o (unchecked-get self "_o")
        identity (unchecked-get o "identity")]
    (if (nil? identity)
      (js/Promise.resolve nil)
      (let [base raw-base]
        (if (or (nil? base)
                (not (identical? 1 (unchecked-get base "v")))
                (not (identical? (unchecked-get base "g") (unchecked-get o "g")))
                (not (js/Array.isArray (unchecked-get base "sc"))))
          (js/Promise.resolve nil)
          (js-await [rh (receipts/room-hash (unchecked-get o "roomId"))]
            (if-not (identical? (unchecked-get base "r") rh)
              nil ; different room — never sign
              (let [my-pub (unchecked-get identity "publicKeyB64")
                    mine (.find ^js (unchecked-get base "sc")
                                (fn [e] (and (some? e) (identical? (unchecked-get e "p") my-pub))))
                    believed ((unchecked-get o "myScore"))]
                (if (or (identical? mine js/undefined)
                        (nil? believed)
                        (not (identical? (unchecked-get mine "s") believed)))
                  nil ; wrong about ME — refuse
                  (js-await [sig (receipts/sign-receipt identity base)]
                    (.send ^js (unchecked-get o "net") from
                           (js-obj "t" "lbs"
                                   "p" (unchecked-get sig "p")
                                   "s" (unchecked-get sig "s")))
                    nil))))))))))

(defn- lb-host-match-end
  "HOST: a match just ended — build the base from the roster and ask the
  players present to co-sign. `scores` = [peerId, nick, score]."
  [self scores]
  (let [o (unchecked-get self "_o")
        identity (unchecked-get o "identity")]
    (if (nil? identity)
      (js/Promise.resolve nil)
      (let [my-pub (unchecked-get identity "publicKeyB64")
            net (unchecked-get o "net")
            sc #js []]
        (dotimes [i (.-length ^js scores)]
          (let [row (aget scores i)
                peer (aget row 0)
                nick (aget row 1)
                score (aget row 2)
                pub (if (identical? peer (unchecked-get net "myId"))
                      my-pub
                      (let [ident (.identityOf ^js net peer)]
                        (if (some? ident) (unchecked-get ident "pub") nil)))]
            ;; identity-less or duplicate identity — JS truthiness like the TS
            ;; original's `!pub`, so an empty-string pub never reaches the wire
            (when (and ^boolean (js* "!!(~{})" pub)
                       (not (.some sc (fn [e] (identical? (unchecked-get e "p") pub)))))
              (.push sc (js-obj "p" pub "nm" (.slice ^js nick 0 32) "s" score)))))
        (if (< (.-length sc) receipts/MIN-SIGNERS)
          (js/Promise.resolve nil) ; unco-signable — not on the board
          (js-await [rh (receipts/room-hash (unchecked-get o "roomId"))]
            (let [mode (unchecked-get o "mode")
                  base (js-obj "v" 1
                               "g" (unchecked-get o "g")
                               "m" (if (nil? mode) "match" mode)
                               "r" rh
                               "ts" (js/Date.now)
                               "n" (.-length ^js scores)
                               "sc" sc)]
              (unchecked-set self "_pending" base)
              (unchecked-set self "_sigs" (js/Map.))
              (js-await [mine (receipts/sign-receipt identity base)]
                (.set ^js (unchecked-get self "_sigs") (unchecked-get mine "p") (unchecked-get mine "s"))
                (.broadcast ^js net (js-obj "t" "lbq" "base" base))
                (js/clearTimeout (unchecked-get self "_timer"))
                (unchecked-set self "_timer"
                               (js/setTimeout (fn [] (lb-finalize self)) SIG-COLLECT-MS))
                nil))))))))

(let [proto (.-prototype GameLb)]
  (unchecked-set proto "hostMatchEnd"
                 (fn [scores]
                   (this-as self (lb-host-match-end self scores))))
  ;; Route a wire frame; true = it was a leaderboard frame (consumed).
  (unchecked-set proto "onFrame"
                 (fn [from frame]
                   (this-as self
                     (let [t (when (some? frame) (unchecked-get frame "t"))]
                       (cond
                         (not (string? t)) false
                         (identical? t "lbq")
                         (do (lb-co-sign self from (unchecked-get frame "base")) true)
                         (identical? t "lbs")
                         (do (when (and (some? (unchecked-get self "_pending"))
                                        (string? (unchecked-get frame "p"))
                                        (string? (unchecked-get frame "s"))
                                        (< (.-size ^js (unchecked-get self "_sigs")) 32))
                               ;; junk is filtered by the final verify
                               (.set ^js (unchecked-get self "_sigs")
                                     (unchecked-get frame "p") (unchecked-get frame "s")))
                             true)
                         (identical? t "lbr")
                         (do (lb-accept self (unchecked-get frame "rec")) true)
                         :else false))))))

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/social-kit.git