OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / integration / layer2_peer_b.gd

158 lines · gd ref:
extends SceneTree
## Layer 2 — Peer B: DHT Discoverer + Connector
##
## Matches JS test pattern: both sides wait for full round-trip.
## 1. join_dht → bootstrap into local testnet
## 2. lookup_topic → discover Peer A
## 3. Wait for dht_peer_found signal
## 4. connect_to_peer(remote_pk) → DHT handshake + holepunch
## 5. Wait for peer_connected
## 6. Exchange packets (A sends → B echoes → A receives → A sends "done" → B receives)

const TIMEOUT := 30.0
const STATUS_FILE := "/tmp/layer2_peer_b_status.txt"
const POLL_HZ := 0.05

var _peer: Object
var _done := false
var _connected := false
var _remote_pk := PackedByteArray()
var _connect_attempted := false
var _sent_reply := false


func _init() -> void:
	_write_status("STARTING")
	_start.call_deferred()


func _start() -> void:
	# Read bootstrap
	var f := FileAccess.open("/tmp/layer_bootstrap.txt", FileAccess.READ)
	if f == null:
		_fail("Cannot read bootstrap")
		return
	var bootstrap := PackedStringArray(f.get_as_text().strip_edges().split(","))
	f.close()
	print("[B] bootstrap: %s" % str(bootstrap))

	# Single peer — DHT mode
	_peer = ClassDB.instantiate(&"HyperswarmMultiplayerPeer")
	var kp: Dictionary = _peer.generate_dht_keypair()
	var pk: PackedByteArray = kp["public_key"]
	var sk: PackedByteArray = kp["secret_key"]
	print("[B] dht pk: %s" % pk.hex_encode().left(16))

	# Connect signals
	_peer.peer_connected.connect(func(id: int) -> void:
		print("[B] peer_connected: id=%d" % id)
		_connected = true
	)
	_peer.dht_peer_found.connect(func(found_pk: PackedByteArray) -> void:
		print("[B] dht_peer_found: pk=%s" % found_pk.hex_encode().left(16))
		if _remote_pk.is_empty():
			_remote_pk = found_pk
	)

	# Join DHT
	var err = _peer.join_dht(pk, sk, bootstrap)
	if err != OK:
		_fail("join_dht: %d" % err)
		return

	# Wait for DHT to become active
	var elapsed := 0.0
	while elapsed < 10.0:
		_peer.poll()
		if _peer.get_dht_status().get("active", false):
			break
		await create_timer(POLL_HZ).timeout
		elapsed += POLL_HZ

	if not _peer.get_dht_status().get("active", false):
		_fail("DHT not active after 10s")
		return

	print("[B] DHT active")

	# Lookup topic to discover Peer A
	var topic := "test-p2p-layer2".sha256_buffer()
	err = _peer.lookup_topic(topic)
	if err != OK:
		_fail("lookup_topic: %d" % err)
		return

	_write_status("DISCOVERING")
	print("[B] looking up topic, waiting for peers...")

	# Main poll loop — matches JS pattern: wait for full round-trip
	elapsed = 0.0
	while elapsed < TIMEOUT and not _done:
		_peer.poll()

		# Once we discover Peer A, connect to them
		if not _remote_pk.is_empty() and not _connect_attempted:
			_connect_attempted = true
			print("[B] connecting to peer: %s" % _remote_pk.hex_encode().left(16))
			err = _peer.connect_to_peer(_remote_pk)
			if err != OK:
				_fail("connect_to_peer: %d" % err)
				return
			_write_status("CONNECTING")

		# Once connected, handle packet exchange
		if _connected:
			while _peer.get_available_packet_count() > 0:
				var pkt: PackedByteArray = _peer.get_packet()
				var text := pkt.get_string_from_utf8()
				print("[B] received: '%s'" % text)
				if text == "hello from A" and not _sent_reply:
					# Echo back (JS pattern: server echoes)
					_sent_reply = true
					var msg := "hello from B".to_utf8_buffer()
					_peer.set_target_peer(1)
					_peer.set_transfer_mode(MultiplayerPeer.TRANSFER_MODE_RELIABLE)
					_peer.put_packet(msg)
					print("[B] sent 'hello from B'")
				elif text == "done":
					# A confirmed receipt — full round-trip complete
					print("[B] received 'done' — full round-trip confirmed")
					_write_status("PASS")
					print("[B] SUCCESS — P2P packet exchange complete")
					_done = true
					break

		await create_timer(POLL_HZ).timeout
		elapsed += POLL_HZ

	if not _done:
		var diag := "remote_pk=%s connected=%s attempted=%s reply_sent=%s" % [
			"found" if not _remote_pk.is_empty() else "none",
			str(_connected), str(_connect_attempted), str(_sent_reply)]
		_fail("Timeout after %.1fs (%s)" % [elapsed, diag])
	else:
		# Flush remaining then close
		for i in range(10):
			_peer.poll()
			await create_timer(POLL_HZ).timeout
		_peer.close()
	quit()


func _write_file(path: String, content: String) -> void:
	var wf := FileAccess.open(path, FileAccess.WRITE)
	wf.store_string(content)
	wf.close()


func _write_status(status: String) -> void:
	_write_file(STATUS_FILE, status)


func _fail(msg: String) -> void:
	print("[B] FAIL: %s" % msg)
	_write_status("FAIL: %s" % msg)
	if _peer:
		_peer.close()
	quit()