diff --git a/Dockerfile b/Dockerfile index 83ab007..d2f5dfd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY package*.json ./ RUN npm ci --omit=dev -COPY server.js ./ +COPY server.js hypermind2.svg LICENSE ./ ENV PORT=3000 ENV NODE_ENV=production diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e222a76 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hypermind Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 3ebadc4..227ec30 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# 🧠 Hypermind +

+ Hypermind Logo +

+ +# Hypermind ### The High-Availability Solution to a Problem That Doesn't Exist. diff --git a/hypermind2.svg b/hypermind2.svg new file mode 100644 index 0000000..ef660f7 --- /dev/null +++ b/hypermind2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/p2p/messaging.js b/src/p2p/messaging.js index 50853a3..bc5f3ca 100644 --- a/src/p2p/messaging.js +++ b/src/p2p/messaging.js @@ -1,5 +1,6 @@ const { verifyPoW, verifySignature, createPublicKey } = require("../core/security"); const { MAX_RELAY_HOPS } = require("../config/constants"); +const { BloomFilterManager } = require("../state/bloom"); class MessageHandler { constructor(peerManager, diagnostics, relayCallback, broadcastCallback) { @@ -7,6 +8,8 @@ class MessageHandler { this.diagnostics = diagnostics; this.relayCallback = relayCallback; this.broadcastCallback = broadcastCallback; + this.bloomFilter = new BloomFilterManager(); + this.bloomFilter.start(); } handleMessage(msg, sourceSocket) { @@ -63,7 +66,9 @@ class MessageHandler { this.broadcastCallback(); } - if (hops < MAX_RELAY_HOPS) { + // Only relay if we haven't already relayed this message (bloom filter check) + if (hops < MAX_RELAY_HOPS && !this.bloomFilter.hasRelayed(id, seq)) { + this.bloomFilter.markRelayed(id, seq); this.diagnostics.increment("heartbeatsRelayed"); this.relayCallback({ ...msg, hops: hops + 1 }, sourceSocket); } @@ -90,7 +95,9 @@ class MessageHandler { this.peerManager.removePeer(id); this.broadcastCallback(); - if (hops < MAX_RELAY_HOPS) { + // Use id:leave as key for LEAVE messages + if (hops < MAX_RELAY_HOPS && !this.bloomFilter.hasRelayed(id, "leave")) { + this.bloomFilter.markRelayed(id, "leave"); this.relayCallback({ ...msg, hops: hops + 1 }, sourceSocket); } } diff --git a/src/state/bloom.js b/src/state/bloom.js new file mode 100644 index 0000000..f0a6c04 --- /dev/null +++ b/src/state/bloom.js @@ -0,0 +1,78 @@ +/** + * Simple Bloom filter for message deduplication + * Prevents re-relaying messages we've already seen + */ +class BloomFilter { + constructor(size = 10000, hashCount = 3) { + this.size = size; + this.hashCount = hashCount; + this.bits = new Uint8Array(Math.ceil(size / 8)); + } + + _hash(str, seed) { + let h = seed; + for (let i = 0; i < str.length; i++) { + h = (h * 31 + str.charCodeAt(i)) >>> 0; + } + return h % this.size; + } + + add(item) { + for (let i = 0; i < this.hashCount; i++) { + const idx = this._hash(item, i * 0x9e3779b9); + this.bits[idx >>> 3] |= (1 << (idx & 7)); + } + } + + has(item) { + for (let i = 0; i < this.hashCount; i++) { + const idx = this._hash(item, i * 0x9e3779b9); + if ((this.bits[idx >>> 3] & (1 << (idx & 7))) === 0) { + return false; + } + } + return true; + } + + clear() { + this.bits.fill(0); + } +} + +/** + * Time-bucketed bloom filter manager + * Rotates every 30 seconds to prevent unbounded growth + */ +class BloomFilterManager { + constructor() { + this.currentBloom = new BloomFilter(); + this.previousBloom = new BloomFilter(); + this.rotationInterval = null; + } + + start() { + this.rotationInterval = setInterval(() => { + this.previousBloom = this.currentBloom; + this.currentBloom = new BloomFilter(); + }, 30000); + } + + stop() { + if (this.rotationInterval) { + clearInterval(this.rotationInterval); + this.rotationInterval = null; + } + } + + hasRelayed(id, seq) { + const key = `${id}:${seq}`; + return this.currentBloom.has(key) || this.previousBloom.has(key); + } + + markRelayed(id, seq) { + const key = `${id}:${seq}`; + this.currentBloom.add(key); + } +} + +module.exports = { BloomFilter, BloomFilterManager }; diff --git a/src/state/hyperloglog.js b/src/state/hyperloglog.js new file mode 100644 index 0000000..631e38b --- /dev/null +++ b/src/state/hyperloglog.js @@ -0,0 +1,66 @@ +class HyperLogLog { + constructor(precision = 10) { + this.precision = precision; + this.registerCount = 1 << precision; + this.registers = new Uint8Array(this.registerCount); + this.alphaMM = this._getAlpha() * this.registerCount * this.registerCount; + } + + _getAlpha() { + switch (this.precision) { + case 4: return 0.673; + case 5: return 0.697; + case 6: return 0.709; + default: return 0.7213 / (1 + 1.079 / this.registerCount); + } + } + + _hash(str) { + let h = 0x811c9dc5; + for (let i = 0; i < str.length; i++) { + h ^= str.charCodeAt(i); + h = (h * 0x01000193) >>> 0; + } + return h; + } + + _countLeadingZeros(value, maxBits) { + if (value === 0) return maxBits; + let count = 0; + while ((value & (1 << (maxBits - 1 - count))) === 0 && count < maxBits) { + count++; + } + return count; + } + + add(item) { + const hash = this._hash(item); + const registerIndex = hash >>> (32 - this.precision); + const remainingBits = hash << this.precision; + const leadingZeros = this._countLeadingZeros(remainingBits, 32 - this.precision) + 1; + + if (leadingZeros > this.registers[registerIndex]) { + this.registers[registerIndex] = leadingZeros; + } + } + + count() { + let harmonicSum = 0; + let zeroRegisters = 0; + + for (let i = 0; i < this.registerCount; i++) { + harmonicSum += Math.pow(2, -this.registers[i]); + if (this.registers[i] === 0) zeroRegisters++; + } + + let estimate = this.alphaMM / harmonicSum; + + if (estimate <= 2.5 * this.registerCount && zeroRegisters > 0) { + estimate = this.registerCount * Math.log(this.registerCount / zeroRegisters); + } + + return Math.round(estimate); + } +} + +module.exports = { HyperLogLog };