OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_nameplate.gd

242 lines · gd ref:
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()