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
### 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 };