extends SceneTree
## Gate: nameplates show the VERIFIED keet member's ROSTER name (hyper-games #89 §3.3).
##
## Before: `_set_player_name` was an any_peer RPC (any peer could relabel any
## other), the realtime `player:name:` copy went out unreliable and was never
## replayed (late joiners saw "Player"), and the label was whatever the peer said.
##
## Now (addons/keet_social/keet_peer_identity.gd): each peer sends {memberId,
## proof-of-its-mesh-key}; the receiver verifies it against the key the swarm
## handshake authenticated (daemon identity.verify), binds peer → member, and shows
## that member's name from the keet room roster. The stub daemon here accepts a
## proof only for its real (memberId, mesh key) pair — the crypto itself is gated by
## keet-daemon test/identity-attest.
##
## Run with no daemon: HYPERTPS_DAEMON_TCP=127.0.0.1:1.
const PeerIdentity := preload("res://addons/keet_social/keet_peer_identity.gd")
const StubClient := preload("res://scenes/test/_stub_keet_client.gd")
const PK_A := "aaaa000000000000000000000000000000000000000000000000000000000001"
const PK_B := "bbbb000000000000000000000000000000000000000000000000000000000002"
const PK_EVE := "eeee000000000000000000000000000000000000000000000000000000000003"
const MEMBER_A := "q7xmk3pd9wfbyo1ru5ta8zcn4hejsg6ixmk3pd9wfbyo1ru5ta8zc"
const MEMBER_B := "w3nf8ka1yorh5tdcz7mbu9pse4gqxiw3nf8ka1yorh5tdcz7mbu9p"
const PROOF_A := "proof-of-alice-for-aaaa"
const PROOF_B := "proof-of-bob-for-bbbb"
var ok := true
var _ran := false
var _names_changed := 0
var _roster_requests := 0
var _sent: Array = []
func _p(c: bool, m: String) -> void:
print(("ok " if c else "FAIL: ") + m)
if not c:
ok = false
func _process(_delta: float) -> bool:
if _ran:
return true
_ran = true
_run()
print("GATE nameplate: %s" % ("PASS" if ok else "FAIL"))
quit(0 if ok else 1)
return true
## The stub daemon: a proof verifies only for the (memberId, mesh key) it was made for.
func _verify(params: Dictionary) -> Dictionary:
var real := {PROOF_A: [MEMBER_A, PK_A], PROOF_B: [MEMBER_B, PK_B]}
var pair: Variant = real.get(String(params.get("proof", "")))
return {"ok": pair != null and pair[0] == params.get("memberId") and pair[1] == params.get("data")}
func _attest(params: Dictionary) -> Dictionary:
return {"memberId": MEMBER_A, "proof": "proof-for-" + String(params.get("data", ""))}
func _record_send(id: int, member_id: String, proof: String) -> void:
_sent.append([id, member_id, proof])
func _on_names_changed() -> void:
_names_changed += 1
func _on_roster_requested() -> void:
_roster_requests += 1
func _new_pi() -> Node:
var stub := StubClient.new()
stub.handlers = {"identity.verify": _verify, "identity.attest": _attest}
var pi: Node = PeerIdentity.new()
pi.client = stub
pi.send_claim = _record_send
pi.names_changed.connect(_on_names_changed)
pi.roster_requested.connect(_on_roster_requested)
root.add_child(pi)
_names_changed = 0
_roster_requests = 0
_sent.clear()
return pi
func _run() -> void:
_verified_names()
_late_joiner_claim_before_key()
_impersonation()
_rename_and_leave()
_own_claim()
_old_paths_gone()
_keet_service_wiring()
_bot_labels()
func _verified_names() -> void:
print("-- a verified claim shows the ROSTER name")
var pi := _new_pi()
pi.set_peer_key(2, PK_A)
_p(pi.name_for_peer(2) == "anon-aaaa", "before any claim: anon-<4 of mesh key> (got %s)" % pi.name_for_peer(2))
pi.receive_claim(2, MEMBER_A, PROOF_A)
_p(pi.member_of(2) == MEMBER_A, "a valid proof binds peer 2 → alice's member id")
_p(_roster_requests >= 1, "and asks for the roster")
_p(pi.name_for_peer(2) == "anon-q7xm", "not in the roster yet: anon-<4 of member id> (got %s)" % pi.name_for_peer(2))
var before := _names_changed
pi.set_roster([{"memberId": MEMBER_A, "name": "alice"}, {"memberId": MEMBER_B, "name": "bob"}])
_p(_names_changed > before, "a roster update fires names_changed")
_p(pi.name_for_peer(2) == "alice", "the label is the roster name (got %s)" % pi.name_for_peer(2))
pi.set_roster([{"memberId": MEMBER_A, "name": MEMBER_A}])
_p(pi.name_for_peer(2) == "anon-q7xm", "a roster entry with no real name (name = member id) reads anon (got %s)" % pi.name_for_peer(2))
pi.queue_free()
func _late_joiner_claim_before_key() -> void:
print("-- the claim may arrive before the handshake key")
var pi := _new_pi()
pi.set_roster([{"memberId": MEMBER_B, "name": "bob"}])
pi.receive_claim(3, MEMBER_B, PROOF_B)
_p(pi.member_of(3) == "", "not bound without the authenticated key")
pi.set_peer_key(3, PK_B)
_p(pi.member_of(3) == MEMBER_B and pi.name_for_peer(3) == "bob", "bound once the key arrives (got %s)" % pi.name_for_peer(3))
pi.queue_free()
func _impersonation() -> void:
print("-- impersonation")
var pi := _new_pi()
pi.set_roster([{"memberId": MEMBER_A, "name": "alice"}])
pi.set_peer_key(4, PK_EVE)
pi.receive_claim(4, MEMBER_A, PROOF_A)
_p(pi.member_of(4) == "", "alice's real proof replayed from another mesh key is refused")
_p(pi.name_for_peer(4) == "anon-eeee", "eve stays anon (got %s)" % pi.name_for_peer(4))
pi.receive_claim(4, MEMBER_A, "made-up-proof")
_p(pi.member_of(4) == "", "a made-up proof is refused")
pi.set_peer_key(5, PK_B)
pi.receive_claim(5, MEMBER_A, PROOF_B)
_p(pi.member_of(5) == "", "bob's proof claiming to be alice is refused")
pi.receive_claim(5, MEMBER_B, PROOF_B)
_p(pi.member_of(5) == MEMBER_B, "bob's own claim is accepted")
pi.receive_claim(5, MEMBER_A, "made-up-proof")
_p(pi.member_of(5) == "", "a later failed claim UNBINDS (no stale trust)")
pi.queue_free()
func _rename_and_leave() -> void:
print("-- rename, leave")
var pi := _new_pi()
pi.set_roster([{"memberId": MEMBER_A, "name": "alice"}])
pi.set_peer_key(2, PK_A)
pi.receive_claim(2, MEMBER_A, PROOF_A)
_p(pi.name_for_peer(2) == "alice", "alice")
var req := _roster_requests
pi.receive_claim(2, MEMBER_A, PROOF_A)
_p(_roster_requests > req, "a re-sent claim (what a rename sends) refreshes the roster")
pi.set_roster([{"memberId": MEMBER_A, "name": "alice2"}])
_p(pi.name_for_peer(2) == "alice2", "the new roster name shows without a respawn")
pi.forget_peer(2)
_p(pi.member_of(2) == "" and pi.name_for_peer(2) == "anon-p2", "a peer that left is forgotten (got %s)" % pi.name_for_peer(2))
pi.queue_free()
func _own_claim() -> void:
print("-- our own claim")
var pi := _new_pi()
pi.set_my_dht_key(PK_A)
var att: Dictionary = (pi.client as RefCounted).last("identity.attest")
_p(att.get("data", "") == PK_A, "attests our OWN mesh key (%s)" % str(att))
pi._on_peer_connected(7)
_p(_sent.size() == 1 and _sent[0][0] == 7 and _sent[0][1] == MEMBER_A and _sent[0][2] == "proof-for-" + PK_A,
"a newly connected peer gets {memberId, proof} (%s)" % str(_sent))
pi._on_peer_connected(8)
_p(_sent.size() == 2 and _sent[1][0] == 8, "every connection gets it — late joiners included")
pi.set_my_dht_key(PK_A)
_p((pi.client as RefCounted).methods().count("identity.attest") == 1, "the same key is attested once")
pi.queue_free()
func _old_paths_gone() -> void:
print("-- the self-asserted paths are gone")
for p: String in ["res://scenes/player/player.gd", "res://scenes/player/player_procedural.gd"]:
var src: String = FileAccess.get_file_as_string(p)
_p(not src.contains("func _set_player_name"), "%s: no any_peer _set_player_name RPC" % p.get_file())
_p(not src.contains("player:name:"), "%s: no player:name: state" % p.get_file())
_p(src.contains("peer_identity.names_changed"), "%s: follows PeerIdentity.names_changed" % p.get_file())
_p(src.contains("name_for_peer(get_multiplayer_authority())"), "%s: label = name_for_peer(owner)" % p.get_file())
for t: String in ["res://scenes/player/player.tscn", "res://scenes/player/player_procedural.tscn"]:
var tscn: String = FileAccess.get_file_as_string(t)
_p(not tscn.contains("text = \"Player\""), "%s: no \"Player\" default label" % t.get_file())
var gs: Node = root.get_node_or_null(^"/root/GameState")
var routed := false
for r: Dictionary in gs.STATE_ROUTES:
if String(r.get("prefix", "")) == "player:name:":
routed = true
_p(not routed, "player:name: is no longer a route")
func _keet_service_wiring() -> void:
print("-- KeetService owns it")
var keet: Node = root.get_node_or_null(^"/root/KeetService")
var pi: Node = keet.get("peer_identity") if keet else null
_p(pi != null, "KeetService.peer_identity exists")
if pi == null:
return
_p(String(pi.get_path()) == "/root/KeetService/PeerIdentity", "same RPC path on every peer (%s)" % String(pi.get_path()))
keet.current_room_id = "gateroom"
keet._on_members_loaded("gateroom", [{"memberId": MEMBER_B, "name": "bob"}])
_p(pi.roster.size() == 1 and pi.roster[0].name == "bob", "members_loaded feeds the roster")
func _bot_labels() -> void:
print("-- bots: <bound peer>.bot")
var keet: Node = root.get_node_or_null(^"/root/KeetService")
var bots: Node = root.get_node_or_null(^"/root/BotRegistry")
if keet == null or bots == null:
_p(false, "KeetService + BotRegistry present")
return
var pi: Node = keet.peer_identity
var stub := StubClient.new()
stub.handlers = {"identity.verify": _verify}
pi.client = stub
pi.set_roster([{"memberId": MEMBER_A, "name": "alice"}])
pi.set_peer_key(2, PK_A)
pi.receive_claim(2, MEMBER_A, PROOF_A)
var bound: Node3D = bots._make_marker("scout1", ["map", "peer:2"], "position", "none")
var unbound: Node3D = bots._make_marker("builder1", ["map"], "position", "none")
var bl: Label3D = bound.get_node("Label")
var ul: Label3D = unbound.get_node("Label")
_p(bl.text.begins_with("alice.bot\n"), "a peer-bound bot is <peer name>.bot (got %s)" % bl.text.split("\n")[0])
_p(ul.text.begins_with("builder1.bot\n"), "an unbound bot is <id>.bot (got %s)" % ul.text.split("\n")[0])
_p(bl.text.split("\n").size() == 2, "the second line (binding · body · brain) stays")
bots._bots["scout1"] = {"bindings": ["map", "peer:2"], "body": "position", "brain": {}, "node": bound}
pi.set_roster([{"memberId": MEMBER_A, "name": "alice2"}])
_p(bl.text.begins_with("alice2.bot\n"), "it follows a rename (got %s)" % bl.text.split("\n")[0])
bots._bots.erase("scout1")
pi.forget_peer(2)
bound.free()
unbound.free()