OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git commits / 6b2b18b

Fix P2P stale refs: use server identity keypair for Hyperswarm
Root cause: git swarm used a random Hyperswarm keypair, so clients couldn't distinguish the origin server from federation peers. When multiple servers announced the same topic (e.g., tabertron + stortron both have hyper-tps.git), clients connected to whichever responded first — often a federation peer with stale refs. Two changes: 1. Server: recreate git swarm with ServerIdentity keypair in initP2P(). Now conn.remotePublicKey matches the server key in punch:// URLs. 2. Client: TopicConnector.connect() verifies conn.remotePublicKey matches the expected server key. Rejects wrong peers and waits for the correct one (with timeout). Also adds REFS debug logging to git-upload-pack proxy for diagnosing future ref propagation issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6b2b18ba24b4f22c8f658d10ace7e908993428b4
by plankatron <fleeky@gmail.com> · 2026-04-07T03:31:50.000Z

Changed files (3) +40 -2

lib/p2p/discovery/topic-manager.js +14 -2
……@@ -214,6 +214,7 @@ export class TopicConnector {
214214 */
215215 async connect(serverKey, repoName) {
216216 const topic = ServerIdentity.deriveTopicFromKey(serverKey, repoName)
217 const expectedKey = Buffer.isBuffer(serverKey) ? serverKey : Buffer.from(serverKey, 'hex')
217218
218219 return new Promise((resolve, reject) => {
219220 const timeout = setTimeout(() => {
……@@ -222,13 +223,24 @@ export class TopicConnector {
222223
223224 this.swarm.join(topic, { server: false, client: true })
224225
225 this.swarm.once('connection', (conn, peerInfo) => {
226 const onConnection = (conn, peerInfo) => {
227 // Verify we connected to the expected server, not a federation peer
228 // or the local machine's own pear-git serving stale data.
229 if (!expectedKey.equals(peerInfo.publicKey)) {
230 console.error(`[TopicConnector] Wrong peer ${peerInfo.publicKey.toString('hex').slice(0, 8)}... (expected ${expectedKey.toString('hex').slice(0, 8)}...), retrying`)
231 conn.destroy()
232 return // wait for next connection
233 }
226234 clearTimeout(timeout)
235 this.swarm.off('connection', onConnection)
227236 resolve(conn)
228 })
237 }
238
239 this.swarm.on('connection', onConnection)
229240
230241 this.swarm.once('error', (err) => {
231242 clearTimeout(timeout)
243 this.swarm.off('connection', onConnection)
232244 reject(err)
233245 })
234246 })
lib/p2p/protocol/git-p2p-handler.js +7 -0
……@@ -791,8 +791,15 @@ export class GitP2PHandler {
791791 gitProcess.stdin.write(chunk)
792792 })
793793
794 let firstChunk = true
794795 gitProcess.stdout.on('data', (chunk) => {
795796 console.log(`[GitP2P] git->conn: ${chunk.length} bytes`)
797 if (firstChunk) {
798 // Log first 200 bytes of ref advertisement for debugging stale refs
799 const preview = chunk.slice(0, 200).toString('utf-8').replace(/\0/g, '\\0')
800 console.log(`[GitP2P] REFS: ${preview}`)
801 firstChunk = false
802 }
796803 conn.write(chunk)
797804 })
798805
pear-git-server.js +19 -0
……@@ -1155,6 +1155,25 @@ class PearGitServer {
11551155 console.log(' Keet identity: NOT SET \u2014 run `pgh identity setup`')
11561156 }
11571157
1158 // Recreate git swarm with server identity keypair so clients can verify
1159 // conn.remotePublicKey matches the expected server key from punch:// URLs.
1160 // Without this, any peer announcing the same topic (including federation
1161 // peers with stale data) could serve instead of the origin server.
1162 if (this.serverIdentity.publicKey && this.serverIdentity.secretKey) {
1163 const oldSwarm = this.swarm
1164 const swarmOpts = this._bootstrap ? { bootstrap: this._bootstrap } : {}
1165 swarmOpts.keyPair = {
1166 publicKey: this.serverIdentity.publicKey,
1167 secretKey: this.serverIdentity.secretKey
1168 }
1169 this.swarm = new Hyperswarm(swarmOpts)
1170 this.swarm.on('error', (err) => {
1171 console.error('[GitSwarm] Error:', err.message)
1172 })
1173 oldSwarm.destroy().catch(() => {})
1174 console.log(`[P2P] Git swarm using server identity key: ${this.serverIdentity.getPublicKeyHex().slice(0, 8)}...`)
1175 }
1176
11581177 this.gitP2PHandler = new GitP2PHandler({
11591178 reposPath: this.repoRegistry.gitReposPath,
11601179 authManager: this.authManager,