yakmesh 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/mesh/message-validator.js +95 -0
- package/mesh/replay-defense.js +138 -0
- package/mesh/sybil-defense.js +151 -0
- package/mesh/temporal-encoder.js +383 -0
- package/package.json +1 -1
- package/test-security.mjs +223 -0
- package/test-tme.mjs +383 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,65 @@ All notable changes to YAKMESH™ will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **TME (Temporal Mesh Encoding)**: Novel packet resilience system unique to YAKMESH
|
|
12
|
+
- Exploits atomic time synchronization as the redundancy dimension
|
|
13
|
+
- Cryptographic temporal chaining binds data to specific points in time
|
|
14
|
+
- Mesh topology-aware encoding for intelligent path diversity
|
|
15
|
+
- NOT erasure coding - a fundamentally new approach to packet loss recovery
|
|
16
|
+
- **TemporalSlice**: Atomic unit of TME with cryptographic time binding
|
|
17
|
+
- Temporal hash includes: data + timestamp + sequence + mesh position
|
|
18
|
+
- Chain integrity via prevTemporalHash linking
|
|
19
|
+
- Tamper detection on deserialization
|
|
20
|
+
- **TemporalStream**: Message slicing and reassembly with temporal properties
|
|
21
|
+
- Configurable slice size and timing intervals
|
|
22
|
+
- Completion tracking and missing slice detection
|
|
23
|
+
- Temporal chain validation
|
|
24
|
+
- **TemporalReconstructor**: Recovery system using timing proofs
|
|
25
|
+
- Consensus verification from multiple mesh neighbors
|
|
26
|
+
- Missing slice attestation via timing proofs
|
|
27
|
+
- Partial reconstruction capabilities
|
|
28
|
+
- **TemporalMeshEncoder**: High-level API for TME operations
|
|
29
|
+
- Full encode/decode lifecycle management
|
|
30
|
+
- Statistics tracking (slices sent/received, completion rates)
|
|
31
|
+
- Stream status monitoring
|
|
32
|
+
- New test suite: 18 TME-specific tests (test-tme.mjs)
|
|
33
|
+
|
|
34
|
+
### Philosophy
|
|
35
|
+
- "Time IS the redundancy dimension" - unlike Walrus/Red Stuff 2D erasure coding
|
|
36
|
+
- Designed for real-time mesh networks with atomic clock sync
|
|
37
|
+
- Leverages YAKMESH's unique post-quantum + atomic timing combination
|
|
38
|
+
|
|
39
|
+
## [1.1.0] - 2026-01-14
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
- **NAVR (Network Assimilation Validation Routine)**: Computational identity verification for new nodes
|
|
43
|
+
- Replaces traditional "Proof of Work" terminology to avoid blockchain confusion
|
|
44
|
+
- One-time puzzle solve during node registration (NOT mining)
|
|
45
|
+
- Configurable difficulty for network defense scaling
|
|
46
|
+
- **Sybil Defense Module** (`mesh/sybil-defense.js`):
|
|
47
|
+
- NAVR computational puzzle for identity creation
|
|
48
|
+
- ReputationTracker for trust scoring (0.0 to 1.0 scale)
|
|
49
|
+
- SubnetDiversity to prevent eclipse attacks (max 3 connections per /24 subnet)
|
|
50
|
+
- **Replay Defense Module** (`mesh/replay-defense.js`):
|
|
51
|
+
- NonceRegistry with cryptographic 32-byte nonces (1hr expiry)
|
|
52
|
+
- TimestampValidator (10-minute freshness window)
|
|
53
|
+
- SequenceTracker for per-sender message ordering
|
|
54
|
+
- ChallengeResponse for mutual node authentication
|
|
55
|
+
- **Message Validator Module** (`mesh/message-validator.js`):
|
|
56
|
+
- Size limits per message type (1MB max, gossip 64KB, handshake 8KB)
|
|
57
|
+
- Nesting depth protection (max 10 levels)
|
|
58
|
+
- SafeJsonParser with prototype pollution protection
|
|
59
|
+
- Expanded test suite: 24 security tests covering all attack vectors
|
|
60
|
+
|
|
61
|
+
### Security
|
|
62
|
+
- Protection against Sybil attacks via NAVR + reputation + subnet diversity
|
|
63
|
+
- Protection against replay attacks via nonces + timestamps + sequences
|
|
64
|
+
- Protection against amplification attacks via message size limits
|
|
65
|
+
- Protection against eclipse attacks via subnet connection limits
|
|
66
|
+
|
|
8
67
|
## [1.0.3] - 2026-01-15
|
|
9
68
|
|
|
10
69
|
### Fixed
|
|
@@ -61,3 +120,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
61
120
|
[1.0.2]: https://github.com/yakmesh/yakmesh/releases/tag/v1.0.2
|
|
62
121
|
[1.0.1]: https://github.com/yakmesh/yakmesh/releases/tag/v1.0.1
|
|
63
122
|
[1.0.0]: https://github.com/yakmesh/yakmesh/releases/tag/v1.0.0
|
|
123
|
+
|
|
124
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Validation Module
|
|
3
|
+
* @module mesh/message-validator.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const SIZE_LIMITS = {
|
|
7
|
+
maxMessageSize: 1024 * 1024,
|
|
8
|
+
maxPayloadSizes: { gossip: 64 * 1024, handshake: 8 * 1024, listing: 128 * 1024, data: 512 * 1024 },
|
|
9
|
+
maxDepth: 10,
|
|
10
|
+
maxArrayLength: 1000,
|
|
11
|
+
maxStringLength: 100000,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class MessageValidator {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.limits = { ...SIZE_LIMITS, ...options.limits };
|
|
17
|
+
this.stats = { validated: 0, rejected: 0, rejectionReasons: new Map() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validateRaw(rawMessage, type = 'gossip') {
|
|
21
|
+
const size = typeof rawMessage === 'string' ? Buffer.byteLength(rawMessage, 'utf8') : rawMessage.length;
|
|
22
|
+
if (size > this.limits.maxMessageSize) return this._reject('Message too large (' + size + ' > ' + this.limits.maxMessageSize + ')');
|
|
23
|
+
const typeLimit = this.limits.maxPayloadSizes[type] || this.limits.maxPayloadSizes.gossip;
|
|
24
|
+
if (size > typeLimit) return this._reject(type + ' message too large (' + size + ' > ' + typeLimit + ')');
|
|
25
|
+
this.stats.validated++;
|
|
26
|
+
return { valid: true, size };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
validateStructure(message, type = 'gossip') {
|
|
30
|
+
if (message === null || message === undefined) return this._reject('Message is null or undefined');
|
|
31
|
+
if (typeof message !== 'object') return this._reject('Expected object, got ' + typeof message);
|
|
32
|
+
const depthCheck = this._checkDepth(message, 0);
|
|
33
|
+
if (!depthCheck.valid) return depthCheck;
|
|
34
|
+
const requiredCheck = this._checkRequiredFields(message, type);
|
|
35
|
+
if (!requiredCheck.valid) return requiredCheck;
|
|
36
|
+
this.stats.validated++;
|
|
37
|
+
return { valid: true };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_checkDepth(obj, depth) {
|
|
41
|
+
if (depth > this.limits.maxDepth) return this._reject('Message nesting too deep');
|
|
42
|
+
if (Array.isArray(obj)) {
|
|
43
|
+
if (obj.length > this.limits.maxArrayLength) return this._reject('Array too long');
|
|
44
|
+
for (const item of obj) {
|
|
45
|
+
if (typeof item === 'object' && item !== null) {
|
|
46
|
+
const check = this._checkDepth(item, depth + 1);
|
|
47
|
+
if (!check.valid) return check;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
51
|
+
for (const value of Object.values(obj)) {
|
|
52
|
+
if (typeof value === 'string' && value.length > this.limits.maxStringLength) return this._reject('String too long');
|
|
53
|
+
if (typeof value === 'object' && value !== null) {
|
|
54
|
+
const check = this._checkDepth(value, depth + 1);
|
|
55
|
+
if (!check.valid) return check;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { valid: true };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_checkRequiredFields(message, type) {
|
|
63
|
+
const requirements = { handshake: ['type', 'nodeId'], gossip: ['type'], listing: ['type', 'data', 'signature'], data: ['type', 'payload'] };
|
|
64
|
+
const required = requirements[type] || requirements.gossip;
|
|
65
|
+
for (const field of required) { if (!(field in message)) return this._reject('Missing required field: ' + field); }
|
|
66
|
+
return { valid: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_reject(reason) {
|
|
70
|
+
this.stats.rejected++;
|
|
71
|
+
const count = this.stats.rejectionReasons.get(reason) || 0;
|
|
72
|
+
this.stats.rejectionReasons.set(reason, count + 1);
|
|
73
|
+
return { valid: false, reason };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getStats() {
|
|
77
|
+
return { validated: this.stats.validated, rejected: this.stats.rejected, topReasons: [...this.stats.rejectionReasons.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10) };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class SafeJsonParser {
|
|
82
|
+
constructor(options = {}) {
|
|
83
|
+
this.maxSize = options.maxSize || SIZE_LIMITS.maxMessageSize;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
parse(json) {
|
|
87
|
+
if (typeof json !== 'string') return { success: false, error: 'Input must be a string' };
|
|
88
|
+
if (json.length > this.maxSize) return { success: false, error: 'JSON too large' };
|
|
89
|
+
if (/__proto__|constructor.*prototype/i.test(json)) return { success: false, error: 'Suspicious content detected' };
|
|
90
|
+
try { return { success: true, data: JSON.parse(json) }; }
|
|
91
|
+
catch (e) { return { success: false, error: 'JSON parse error: ' + e.message }; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default MessageValidator;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay Attack Protection Module
|
|
3
|
+
* @module mesh/replay-defense.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { bytesToHex, randomBytes } from '@noble/hashes/utils.js';
|
|
7
|
+
|
|
8
|
+
export class NonceRegistry {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.maxAge = options.maxAge || 3600000;
|
|
11
|
+
this.maxSize = options.maxSize || 100000;
|
|
12
|
+
this.nonces = new Map();
|
|
13
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
generate() {
|
|
17
|
+
const nonce = bytesToHex(randomBytes(32));
|
|
18
|
+
this.nonces.set(nonce, Date.now());
|
|
19
|
+
return nonce;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
validate(nonce) {
|
|
23
|
+
if (!nonce || typeof nonce !== 'string') return { valid: false, reason: 'Missing or invalid nonce' };
|
|
24
|
+
if (nonce.length !== 64) return { valid: false, reason: 'Invalid nonce length' };
|
|
25
|
+
if (this.nonces.has(nonce)) return { valid: false, reason: 'Nonce already used (replay detected)' };
|
|
26
|
+
this.nonces.set(nonce, Date.now());
|
|
27
|
+
if (this.nonces.size > this.maxSize) this.cleanup();
|
|
28
|
+
return { valid: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cleanup() {
|
|
32
|
+
const cutoff = Date.now() - this.maxAge;
|
|
33
|
+
for (const [nonce, ts] of this.nonces) { if (ts < cutoff) this.nonces.delete(nonce); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getStats() { return { trackedNonces: this.nonces.size, maxSize: this.maxSize }; }
|
|
37
|
+
destroy() { clearInterval(this.cleanupInterval); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TimestampValidator {
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.maxAge = options.maxAge || 600000;
|
|
43
|
+
this.maxFuture = options.maxFuture || 60000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
validate(messageTime) {
|
|
47
|
+
if (!messageTime || typeof messageTime !== 'number') return { valid: false, reason: 'Missing or invalid timestamp' };
|
|
48
|
+
const drift = Date.now() - messageTime;
|
|
49
|
+
if (drift > this.maxAge) return { valid: false, reason: 'Message too old (' + Math.round(drift/1000) + 's)', drift };
|
|
50
|
+
if (drift < -this.maxFuture) return { valid: false, reason: 'Message from future', drift };
|
|
51
|
+
return { valid: true, drift };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
create() { return Date.now(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class SequenceTracker {
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.maxSenders = options.maxSenders || 10000;
|
|
60
|
+
this.senders = new Map();
|
|
61
|
+
this.windowSize = options.windowSize || 64;
|
|
62
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 300000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
validate(senderId, seq) {
|
|
66
|
+
if (!senderId) return { valid: false, reason: 'Missing sender ID' };
|
|
67
|
+
if (typeof seq !== 'number' || seq < 0) return { valid: false, reason: 'Invalid sequence number' };
|
|
68
|
+
|
|
69
|
+
let sender = this.senders.get(senderId);
|
|
70
|
+
if (!sender) {
|
|
71
|
+
sender = { lastSeq: seq, lastSeen: Date.now(), window: new Set([seq]) };
|
|
72
|
+
this.senders.set(senderId, sender);
|
|
73
|
+
return { valid: true, isNew: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sender.lastSeen = Date.now();
|
|
77
|
+
if (sender.window.has(seq)) return { valid: false, reason: 'Duplicate sequence (replay)' };
|
|
78
|
+
if (seq < sender.lastSeq - this.windowSize) return { valid: false, reason: 'Sequence too old' };
|
|
79
|
+
|
|
80
|
+
sender.window.add(seq);
|
|
81
|
+
if (seq > sender.lastSeq) sender.lastSeq = seq;
|
|
82
|
+
if (sender.window.size > this.windowSize * 2) {
|
|
83
|
+
const minSeq = sender.lastSeq - this.windowSize;
|
|
84
|
+
for (const s of sender.window) { if (s < minSeq) sender.window.delete(s); }
|
|
85
|
+
}
|
|
86
|
+
return { valid: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
nextSequence(peerId) {
|
|
90
|
+
let sender = this.senders.get(peerId);
|
|
91
|
+
if (!sender) { sender = { lastSeq: 0, lastSeen: Date.now(), window: new Set() }; this.senders.set(peerId, sender); }
|
|
92
|
+
return ++sender.lastSeq;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cleanup() {
|
|
96
|
+
const staleTime = 86400000;
|
|
97
|
+
for (const [id, s] of this.senders) { if (Date.now() - s.lastSeen > staleTime) this.senders.delete(id); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getStats() { return { trackedSenders: this.senders.size }; }
|
|
101
|
+
destroy() { clearInterval(this.cleanupInterval); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export class ReplayDefense {
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
this.nonces = new NonceRegistry(options.nonces || {});
|
|
107
|
+
this.timestamps = new TimestampValidator(options.timestamps || {});
|
|
108
|
+
this.sequences = new SequenceTracker(options.sequences || {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
validateMessage(message) {
|
|
112
|
+
const details = {};
|
|
113
|
+
const timeCheck = this.timestamps.validate(message.timestamp);
|
|
114
|
+
details.timestamp = timeCheck;
|
|
115
|
+
if (!timeCheck.valid) return { valid: false, reason: timeCheck.reason, details };
|
|
116
|
+
|
|
117
|
+
const nonceCheck = this.nonces.validate(message.nonce);
|
|
118
|
+
details.nonce = nonceCheck;
|
|
119
|
+
if (!nonceCheck.valid) return { valid: false, reason: nonceCheck.reason, details };
|
|
120
|
+
|
|
121
|
+
if (message.senderId && message.seq !== undefined) {
|
|
122
|
+
const seqCheck = this.sequences.validate(message.senderId, message.seq);
|
|
123
|
+
details.sequence = seqCheck;
|
|
124
|
+
if (!seqCheck.valid) return { valid: false, reason: seqCheck.reason, details };
|
|
125
|
+
}
|
|
126
|
+
return { valid: true, details };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
prepareMessage(senderId, peerId) {
|
|
130
|
+
return { nonce: this.nonces.generate(), timestamp: this.timestamps.create(), seq: this.sequences.nextSequence(peerId || 'broadcast') };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getStats() { return { nonces: this.nonces.getStats(), sequences: this.sequences.getStats() }; }
|
|
134
|
+
|
|
135
|
+
destroy() { this.nonces.destroy(); this.sequences.destroy(); }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export default ReplayDefense;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sybil Attack Protection Module
|
|
3
|
+
* @module mesh/sybil-defense.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sha3_256 } from '@noble/hashes/sha3.js';
|
|
7
|
+
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js';
|
|
8
|
+
|
|
9
|
+
export class NAVR {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.difficulty = options.difficulty || 16;
|
|
12
|
+
this.maxAttempts = options.maxAttempts || 10_000_000;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
createChallenge(nodeId, timestamp = Date.now()) {
|
|
16
|
+
const epochHour = Math.floor(timestamp / 3600000);
|
|
17
|
+
const challenge = bytesToHex(sha3_256(utf8ToBytes(nodeId + ':' + epochHour)));
|
|
18
|
+
return { nodeId, challenge, epochHour, difficulty: this.difficulty, expiresAt: (epochHour + 1) * 3600000 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
solve(challenge) {
|
|
22
|
+
const target = BigInt(2) ** BigInt(256 - challenge.difficulty);
|
|
23
|
+
for (let nonce = 0; nonce < this.maxAttempts; nonce++) {
|
|
24
|
+
const attempt = challenge.challenge + ':' + nonce;
|
|
25
|
+
const hash = sha3_256(utf8ToBytes(attempt));
|
|
26
|
+
const hashBigInt = BigInt('0x' + bytesToHex(hash));
|
|
27
|
+
if (hashBigInt < target) return { challenge: challenge.challenge, nonce, hash: bytesToHex(hash), attempts: nonce + 1 };
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
verify(challenge, solution) {
|
|
33
|
+
if (!challenge || !solution || challenge.challenge !== solution.challenge) return false;
|
|
34
|
+
if (Date.now() > challenge.expiresAt) return false;
|
|
35
|
+
const attempt = solution.challenge + ':' + solution.nonce;
|
|
36
|
+
const hash = sha3_256(utf8ToBytes(attempt));
|
|
37
|
+
const hashHex = bytesToHex(hash);
|
|
38
|
+
if (hashHex !== solution.hash) return false;
|
|
39
|
+
const target = BigInt(2) ** BigInt(256 - challenge.difficulty);
|
|
40
|
+
return BigInt('0x' + hashHex) < target;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ReputationTracker {
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.nodes = new Map();
|
|
47
|
+
this.thresholds = { trusted: 0.6, normal: 0.3, suspicious: 0.1, banned: 0.0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
registerNode(nodeId, NAVRSolution = null) {
|
|
51
|
+
if (this.nodes.has(nodeId)) return this.nodes.get(nodeId);
|
|
52
|
+
const record = { nodeId, reputation: NAVRSolution ? 0.3 : 0.1, registeredAt: Date.now(), lastSeen: Date.now(), goodBehaviors: 0, badBehaviors: 0 };
|
|
53
|
+
this.nodes.set(nodeId, record);
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getTrustLevel(nodeId) {
|
|
58
|
+
const r = this.nodes.get(nodeId);
|
|
59
|
+
if (!r) return 'unknown';
|
|
60
|
+
if (r.reputation >= this.thresholds.trusted) return 'trusted';
|
|
61
|
+
if (r.reputation >= this.thresholds.normal) return 'normal';
|
|
62
|
+
if (r.reputation >= this.thresholds.suspicious) return 'suspicious';
|
|
63
|
+
return 'banned';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
reportGoodBehavior(nodeId, weight = 0.01) {
|
|
67
|
+
const r = this.nodes.get(nodeId);
|
|
68
|
+
if (r) { r.goodBehaviors++; r.lastSeen = Date.now(); r.reputation = Math.min(1.0, r.reputation + weight); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reportBadBehavior(nodeId, weight = 0.1) {
|
|
72
|
+
const r = this.nodes.get(nodeId);
|
|
73
|
+
if (r) { r.badBehaviors++; r.reputation = Math.max(0.0, r.reputation - weight); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getStats() {
|
|
77
|
+
let trusted = 0, normal = 0, suspicious = 0, banned = 0;
|
|
78
|
+
for (const r of this.nodes.values()) {
|
|
79
|
+
const l = this.getTrustLevel(r.nodeId);
|
|
80
|
+
if (l === 'trusted') trusted++; else if (l === 'normal') normal++; else if (l === 'suspicious') suspicious++; else banned++;
|
|
81
|
+
}
|
|
82
|
+
return { total: this.nodes.size, trusted, normal, suspicious, banned };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class SubnetDiversity {
|
|
87
|
+
constructor(options = {}) {
|
|
88
|
+
this.maxPerSubnet = options.maxPerSubnet || 3;
|
|
89
|
+
this.subnets = new Map();
|
|
90
|
+
this.connections = new Map();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getSubnet(ip) {
|
|
94
|
+
if (!ip) return 'unknown';
|
|
95
|
+
if (ip.includes('::ffff:')) ip = ip.split('::ffff:')[1];
|
|
96
|
+
if (ip.includes('.')) return ip.split('.').slice(0, 3).join('.');
|
|
97
|
+
return 'unknown';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
allowConnection(ip) {
|
|
101
|
+
const subnet = this.getSubnet(ip);
|
|
102
|
+
const count = this.subnets.get(subnet) || 0;
|
|
103
|
+
if (count >= this.maxPerSubnet) return { allowed: false, reason: 'Too many from subnet ' + subnet };
|
|
104
|
+
return { allowed: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
addConnection(ip, nodeId) {
|
|
108
|
+
const subnet = this.getSubnet(ip);
|
|
109
|
+
this.subnets.set(subnet, (this.subnets.get(subnet) || 0) + 1);
|
|
110
|
+
this.connections.set(ip, nodeId);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
removeConnection(ip) {
|
|
114
|
+
const subnet = this.getSubnet(ip);
|
|
115
|
+
const count = this.subnets.get(subnet) || 0;
|
|
116
|
+
if (count > 0) this.subnets.set(subnet, count - 1);
|
|
117
|
+
this.connections.delete(ip);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class SybilDefense {
|
|
122
|
+
constructor(options = {}) {
|
|
123
|
+
this.NAVR = new NAVR(options.NAVR || {});
|
|
124
|
+
this.reputation = new ReputationTracker(options.reputation || {});
|
|
125
|
+
this.diversity = new SubnetDiversity(options.diversity || {});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
evaluateConnection(ip, nodeId, NAVRSolution = null) {
|
|
129
|
+
const divCheck = this.diversity.allowConnection(ip);
|
|
130
|
+
if (!divCheck.allowed) return { allowed: false, reason: divCheck.reason };
|
|
131
|
+
let record = this.reputation.nodes.get(nodeId);
|
|
132
|
+
if (!record) record = this.reputation.registerNode(nodeId, NAVRSolution);
|
|
133
|
+
const trustLevel = this.reputation.getTrustLevel(nodeId);
|
|
134
|
+
if (trustLevel === 'banned') return { allowed: false, reason: 'Node is banned' };
|
|
135
|
+
if (trustLevel === 'unknown' && !NAVRSolution) return { allowed: false, reason: 'NAVR required', challenge: this.NAVR.createChallenge(nodeId) };
|
|
136
|
+
this.diversity.addConnection(ip, nodeId);
|
|
137
|
+
return { allowed: true, trustLevel, reputation: record.reputation };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
reportMessage(nodeId, valid) {
|
|
141
|
+
if (valid) this.reputation.reportGoodBehavior(nodeId, 0.005);
|
|
142
|
+
else this.reputation.reportBadBehavior(nodeId, 0.05);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
handleDisconnect(ip) { this.diversity.removeConnection(ip); }
|
|
146
|
+
|
|
147
|
+
getStats() { return { reputation: this.reputation.getStats() }; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default SybilDefense;
|
|
151
|
+
|