@sparkleideas/swarm 3.0.0-alpha.7
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/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Byzantine Fault Tolerant Consensus
|
|
3
|
+
* PBFT-style consensus for handling malicious or faulty nodes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import {
|
|
8
|
+
ConsensusProposal,
|
|
9
|
+
ConsensusVote,
|
|
10
|
+
ConsensusResult,
|
|
11
|
+
ConsensusConfig,
|
|
12
|
+
SWARM_CONSTANTS,
|
|
13
|
+
} from '../types.js';
|
|
14
|
+
|
|
15
|
+
export type ByzantinePhase = 'pre-prepare' | 'prepare' | 'commit' | 'reply';
|
|
16
|
+
|
|
17
|
+
export interface ByzantineMessage {
|
|
18
|
+
type: ByzantinePhase;
|
|
19
|
+
viewNumber: number;
|
|
20
|
+
sequenceNumber: number;
|
|
21
|
+
digest: string;
|
|
22
|
+
senderId: string;
|
|
23
|
+
timestamp: Date;
|
|
24
|
+
payload?: unknown;
|
|
25
|
+
signature?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ByzantineNode {
|
|
29
|
+
id: string;
|
|
30
|
+
isPrimary: boolean;
|
|
31
|
+
viewNumber: number;
|
|
32
|
+
sequenceNumber: number;
|
|
33
|
+
preparedMessages: Map<string, ByzantineMessage[]>;
|
|
34
|
+
committedMessages: Map<string, ByzantineMessage[]>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ByzantineConfig extends Partial<ConsensusConfig> {
|
|
38
|
+
maxFaultyNodes?: number;
|
|
39
|
+
viewChangeTimeoutMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ByzantineConsensus extends EventEmitter {
|
|
43
|
+
private config: ByzantineConfig;
|
|
44
|
+
private node: ByzantineNode;
|
|
45
|
+
private nodes: Map<string, ByzantineNode> = new Map();
|
|
46
|
+
private proposals: Map<string, ConsensusProposal> = new Map();
|
|
47
|
+
private messageLog: Map<string, ByzantineMessage[]> = new Map();
|
|
48
|
+
private proposalCounter: number = 0;
|
|
49
|
+
private viewChangeTimeout?: NodeJS.Timeout;
|
|
50
|
+
|
|
51
|
+
constructor(nodeId: string, config: ByzantineConfig = {}) {
|
|
52
|
+
super();
|
|
53
|
+
this.config = {
|
|
54
|
+
threshold: config.threshold ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_THRESHOLD,
|
|
55
|
+
timeoutMs: config.timeoutMs ?? SWARM_CONSTANTS.DEFAULT_CONSENSUS_TIMEOUT_MS,
|
|
56
|
+
maxRounds: config.maxRounds ?? 10,
|
|
57
|
+
requireQuorum: config.requireQuorum ?? true,
|
|
58
|
+
maxFaultyNodes: config.maxFaultyNodes ?? 1,
|
|
59
|
+
viewChangeTimeoutMs: config.viewChangeTimeoutMs ?? 5000,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.node = {
|
|
63
|
+
id: nodeId,
|
|
64
|
+
isPrimary: false,
|
|
65
|
+
viewNumber: 0,
|
|
66
|
+
sequenceNumber: 0,
|
|
67
|
+
preparedMessages: new Map(),
|
|
68
|
+
committedMessages: new Map(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async initialize(): Promise<void> {
|
|
73
|
+
this.emit('initialized', { nodeId: this.node.id });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async shutdown(): Promise<void> {
|
|
77
|
+
if (this.viewChangeTimeout) {
|
|
78
|
+
clearTimeout(this.viewChangeTimeout);
|
|
79
|
+
}
|
|
80
|
+
this.emit('shutdown');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addNode(nodeId: string, isPrimary: boolean = false): void {
|
|
84
|
+
this.nodes.set(nodeId, {
|
|
85
|
+
id: nodeId,
|
|
86
|
+
isPrimary,
|
|
87
|
+
viewNumber: 0,
|
|
88
|
+
sequenceNumber: 0,
|
|
89
|
+
preparedMessages: new Map(),
|
|
90
|
+
committedMessages: new Map(),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (isPrimary && this.node.id === nodeId) {
|
|
94
|
+
this.node.isPrimary = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removeNode(nodeId: string): void {
|
|
99
|
+
this.nodes.delete(nodeId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
electPrimary(): string {
|
|
103
|
+
const nodeIds = [this.node.id, ...Array.from(this.nodes.keys())];
|
|
104
|
+
const primaryIndex = this.node.viewNumber % nodeIds.length;
|
|
105
|
+
const primaryId = nodeIds[primaryIndex];
|
|
106
|
+
|
|
107
|
+
this.node.isPrimary = primaryId === this.node.id;
|
|
108
|
+
|
|
109
|
+
for (const [id, node] of this.nodes) {
|
|
110
|
+
node.isPrimary = id === primaryId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.emit('primary.elected', { primaryId, viewNumber: this.node.viewNumber });
|
|
114
|
+
|
|
115
|
+
return primaryId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async propose(value: unknown): Promise<ConsensusProposal> {
|
|
119
|
+
if (!this.node.isPrimary) {
|
|
120
|
+
throw new Error('Only primary can propose values');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.proposalCounter++;
|
|
124
|
+
const sequenceNumber = ++this.node.sequenceNumber;
|
|
125
|
+
const digest = this.computeDigest(value);
|
|
126
|
+
const proposalId = `bft_${this.node.viewNumber}_${sequenceNumber}`;
|
|
127
|
+
|
|
128
|
+
const proposal: ConsensusProposal = {
|
|
129
|
+
id: proposalId,
|
|
130
|
+
proposerId: this.node.id,
|
|
131
|
+
value,
|
|
132
|
+
term: this.node.viewNumber,
|
|
133
|
+
timestamp: new Date(),
|
|
134
|
+
votes: new Map(),
|
|
135
|
+
status: 'pending',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.proposals.set(proposalId, proposal);
|
|
139
|
+
|
|
140
|
+
// Phase 1: Pre-prepare
|
|
141
|
+
const prePrepareMsg: ByzantineMessage = {
|
|
142
|
+
type: 'pre-prepare',
|
|
143
|
+
viewNumber: this.node.viewNumber,
|
|
144
|
+
sequenceNumber,
|
|
145
|
+
digest,
|
|
146
|
+
senderId: this.node.id,
|
|
147
|
+
timestamp: new Date(),
|
|
148
|
+
payload: value,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await this.broadcastMessage(prePrepareMsg);
|
|
152
|
+
|
|
153
|
+
// Self-prepare
|
|
154
|
+
await this.handlePrepare({
|
|
155
|
+
type: 'prepare',
|
|
156
|
+
viewNumber: this.node.viewNumber,
|
|
157
|
+
sequenceNumber,
|
|
158
|
+
digest,
|
|
159
|
+
senderId: this.node.id,
|
|
160
|
+
timestamp: new Date(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return proposal;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async vote(proposalId: string, vote: ConsensusVote): Promise<void> {
|
|
167
|
+
const proposal = this.proposals.get(proposalId);
|
|
168
|
+
if (!proposal || proposal.status !== 'pending') {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
proposal.votes.set(vote.voterId, vote);
|
|
173
|
+
|
|
174
|
+
// Check consensus
|
|
175
|
+
const f = this.config.maxFaultyNodes ?? 1;
|
|
176
|
+
const n = this.nodes.size + 1;
|
|
177
|
+
const requiredVotes = 2 * f + 1;
|
|
178
|
+
|
|
179
|
+
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
180
|
+
v => v.approve
|
|
181
|
+
).length;
|
|
182
|
+
|
|
183
|
+
if (approvingVotes >= requiredVotes) {
|
|
184
|
+
proposal.status = 'accepted';
|
|
185
|
+
this.emit('consensus.achieved', { proposalId, approved: true });
|
|
186
|
+
} else if (proposal.votes.size >= n && approvingVotes < requiredVotes) {
|
|
187
|
+
proposal.status = 'rejected';
|
|
188
|
+
this.emit('consensus.achieved', { proposalId, approved: false });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async awaitConsensus(proposalId: string): Promise<ConsensusResult> {
|
|
193
|
+
const startTime = Date.now();
|
|
194
|
+
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const checkInterval = setInterval(() => {
|
|
197
|
+
const proposal = this.proposals.get(proposalId);
|
|
198
|
+
if (!proposal) {
|
|
199
|
+
clearInterval(checkInterval);
|
|
200
|
+
reject(new Error(`Proposal ${proposalId} not found`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (proposal.status !== 'pending') {
|
|
205
|
+
clearInterval(checkInterval);
|
|
206
|
+
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (Date.now() - startTime > (this.config.timeoutMs ?? 30000)) {
|
|
211
|
+
clearInterval(checkInterval);
|
|
212
|
+
proposal.status = 'expired';
|
|
213
|
+
resolve(this.createResult(proposal, Date.now() - startTime));
|
|
214
|
+
}
|
|
215
|
+
}, 10);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ===== MESSAGE HANDLERS =====
|
|
220
|
+
|
|
221
|
+
async handlePrePrepare(message: ByzantineMessage): Promise<void> {
|
|
222
|
+
// Validate message
|
|
223
|
+
if (message.viewNumber !== this.node.viewNumber) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Accept pre-prepare from primary
|
|
228
|
+
const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
|
|
229
|
+
|
|
230
|
+
if (!this.proposals.has(proposalId) && message.payload !== undefined) {
|
|
231
|
+
const proposal: ConsensusProposal = {
|
|
232
|
+
id: proposalId,
|
|
233
|
+
proposerId: message.senderId,
|
|
234
|
+
value: message.payload,
|
|
235
|
+
term: message.viewNumber,
|
|
236
|
+
timestamp: message.timestamp,
|
|
237
|
+
votes: new Map(),
|
|
238
|
+
status: 'pending',
|
|
239
|
+
};
|
|
240
|
+
this.proposals.set(proposalId, proposal);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Send prepare message
|
|
244
|
+
const prepareMsg: ByzantineMessage = {
|
|
245
|
+
type: 'prepare',
|
|
246
|
+
viewNumber: message.viewNumber,
|
|
247
|
+
sequenceNumber: message.sequenceNumber,
|
|
248
|
+
digest: message.digest,
|
|
249
|
+
senderId: this.node.id,
|
|
250
|
+
timestamp: new Date(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await this.broadcastMessage(prepareMsg);
|
|
254
|
+
await this.handlePrepare(prepareMsg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async handlePrepare(message: ByzantineMessage): Promise<void> {
|
|
258
|
+
const key = `${message.viewNumber}_${message.sequenceNumber}`;
|
|
259
|
+
|
|
260
|
+
if (!this.messageLog.has(key)) {
|
|
261
|
+
this.messageLog.set(key, []);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const messages = this.messageLog.get(key)!;
|
|
265
|
+
const hasPrepare = messages.some(
|
|
266
|
+
m => m.type === 'prepare' && m.senderId === message.senderId
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (!hasPrepare) {
|
|
270
|
+
messages.push(message);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check if prepared (2f + 1 prepare messages)
|
|
274
|
+
const f = this.config.maxFaultyNodes ?? 1;
|
|
275
|
+
const prepareCount = messages.filter(m => m.type === 'prepare').length;
|
|
276
|
+
|
|
277
|
+
if (prepareCount >= 2 * f + 1) {
|
|
278
|
+
const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
|
|
279
|
+
this.node.preparedMessages.set(key, messages);
|
|
280
|
+
|
|
281
|
+
// Send commit message
|
|
282
|
+
const commitMsg: ByzantineMessage = {
|
|
283
|
+
type: 'commit',
|
|
284
|
+
viewNumber: message.viewNumber,
|
|
285
|
+
sequenceNumber: message.sequenceNumber,
|
|
286
|
+
digest: message.digest,
|
|
287
|
+
senderId: this.node.id,
|
|
288
|
+
timestamp: new Date(),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
await this.broadcastMessage(commitMsg);
|
|
292
|
+
await this.handleCommit(commitMsg);
|
|
293
|
+
|
|
294
|
+
// Record vote
|
|
295
|
+
const proposal = this.proposals.get(proposalId);
|
|
296
|
+
if (proposal) {
|
|
297
|
+
proposal.votes.set(this.node.id, {
|
|
298
|
+
voterId: this.node.id,
|
|
299
|
+
approve: true,
|
|
300
|
+
confidence: 1.0,
|
|
301
|
+
timestamp: new Date(),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async handleCommit(message: ByzantineMessage): Promise<void> {
|
|
308
|
+
const key = `${message.viewNumber}_${message.sequenceNumber}`;
|
|
309
|
+
|
|
310
|
+
if (!this.messageLog.has(key)) {
|
|
311
|
+
this.messageLog.set(key, []);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const messages = this.messageLog.get(key)!;
|
|
315
|
+
const hasCommit = messages.some(
|
|
316
|
+
m => m.type === 'commit' && m.senderId === message.senderId
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!hasCommit) {
|
|
320
|
+
messages.push(message);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check if committed (2f + 1 commit messages)
|
|
324
|
+
const f = this.config.maxFaultyNodes ?? 1;
|
|
325
|
+
const commitCount = messages.filter(m => m.type === 'commit').length;
|
|
326
|
+
|
|
327
|
+
if (commitCount >= 2 * f + 1) {
|
|
328
|
+
const proposalId = `bft_${message.viewNumber}_${message.sequenceNumber}`;
|
|
329
|
+
this.node.committedMessages.set(key, messages);
|
|
330
|
+
|
|
331
|
+
const proposal = this.proposals.get(proposalId);
|
|
332
|
+
if (proposal && proposal.status === 'pending') {
|
|
333
|
+
proposal.status = 'accepted';
|
|
334
|
+
this.emit('consensus.achieved', { proposalId, approved: true });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ===== VIEW CHANGE =====
|
|
340
|
+
|
|
341
|
+
async initiateViewChange(): Promise<void> {
|
|
342
|
+
this.node.viewNumber++;
|
|
343
|
+
|
|
344
|
+
this.emit('view.changing', { newViewNumber: this.node.viewNumber });
|
|
345
|
+
|
|
346
|
+
// Elect new primary
|
|
347
|
+
this.electPrimary();
|
|
348
|
+
|
|
349
|
+
this.emit('view.changed', { viewNumber: this.node.viewNumber });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ===== PRIVATE METHODS =====
|
|
353
|
+
|
|
354
|
+
private async broadcastMessage(message: ByzantineMessage): Promise<void> {
|
|
355
|
+
this.emit('message.broadcast', { message });
|
|
356
|
+
|
|
357
|
+
// Broadcast to all registered nodes
|
|
358
|
+
for (const node of this.nodes.values()) {
|
|
359
|
+
this.emit('message.sent', { to: node.id, message });
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private computeDigest(value: unknown): string {
|
|
364
|
+
// Simple hash for demonstration
|
|
365
|
+
const str = JSON.stringify(value);
|
|
366
|
+
let hash = 0;
|
|
367
|
+
for (let i = 0; i < str.length; i++) {
|
|
368
|
+
const char = str.charCodeAt(i);
|
|
369
|
+
hash = ((hash << 5) - hash) + char;
|
|
370
|
+
hash = hash & hash;
|
|
371
|
+
}
|
|
372
|
+
return hash.toString(16);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private createResult(proposal: ConsensusProposal, durationMs: number): ConsensusResult {
|
|
376
|
+
const n = this.nodes.size + 1;
|
|
377
|
+
const approvingVotes = Array.from(proposal.votes.values()).filter(
|
|
378
|
+
v => v.approve
|
|
379
|
+
).length;
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
proposalId: proposal.id,
|
|
383
|
+
approved: proposal.status === 'accepted',
|
|
384
|
+
approvalRate: proposal.votes.size > 0
|
|
385
|
+
? approvingVotes / proposal.votes.size
|
|
386
|
+
: 0,
|
|
387
|
+
participationRate: proposal.votes.size / n,
|
|
388
|
+
finalValue: proposal.value,
|
|
389
|
+
rounds: 3, // pre-prepare, prepare, commit
|
|
390
|
+
durationMs,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ===== STATE QUERIES =====
|
|
395
|
+
|
|
396
|
+
isPrimary(): boolean {
|
|
397
|
+
return this.node.isPrimary;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
getViewNumber(): number {
|
|
401
|
+
return this.node.viewNumber;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getSequenceNumber(): number {
|
|
405
|
+
return this.node.sequenceNumber;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
getPreparedCount(): number {
|
|
409
|
+
return this.node.preparedMessages.size;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
getCommittedCount(): number {
|
|
413
|
+
return this.node.committedMessages.size;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getMaxFaultyNodes(): number {
|
|
417
|
+
const n = this.nodes.size + 1;
|
|
418
|
+
return Math.floor((n - 1) / 3);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
canTolerate(faultyCount: number): boolean {
|
|
422
|
+
return faultyCount <= this.getMaxFaultyNodes();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function createByzantineConsensus(
|
|
427
|
+
nodeId: string,
|
|
428
|
+
config?: ByzantineConfig
|
|
429
|
+
): ByzantineConsensus {
|
|
430
|
+
return new ByzantineConsensus(nodeId, config);
|
|
431
|
+
}
|