| 1 | """
|
| 2 | SwarmConsensus - Decentralized decision-making for multi-agent systems
|
| 3 | """
|
| 4 | import json
|
| 5 | import hashlib
|
| 6 | import time
|
| 7 | from typing import Dict, List, Optional
|
| 8 | from dataclasses import dataclass, asdict
|
| 9 | from datetime import datetime
|
| 10 |
|
| 11 | @dataclass
|
| 12 | class Vote:
|
| 13 | proposal_id: str
|
| 14 | voter: str
|
| 15 | vote: str # "approve" | "reject" | "abstain"
|
| 16 | weight: float
|
| 17 | timestamp: str
|
| 18 | signature: str # In production: ed25519 signature
|
| 19 |
|
| 20 | @dataclass
|
| 21 | class Proposal:
|
| 22 | id: str
|
| 23 | title: str
|
| 24 | description: str
|
| 25 | code_diff: str
|
| 26 | proposer: str
|
| 27 | created_at: str
|
| 28 | status: str # "pending" | "approved" | "rejected"
|
| 29 | threshold_type: str # "simple" | "supermajority" | "unanimous"
|
| 30 | threshold_value: float # e.g., 0.67 for 67% supermajority
|
| 31 |
|
| 32 | class SwarmConsensus:
|
| 33 | def __init__(self, repo: str):
|
| 34 | self.repo = repo
|
| 35 | self.proposals: Dict[str, Proposal] = {}
|
| 36 | self.votes: Dict[str, List[Vote]] = {}
|
| 37 | self.agent_reputations: Dict[str, float] = {}
|
| 38 |
|
| 39 | def calculate_reputation(self, agent_id: str) -> float:
|
| 40 | """
|
| 41 | Calculate agent reputation weight based on contribution history
|
| 42 | In production: query moltcode.io API for real stats
|
| 43 | """
|
| 44 | if agent_id not in self.agent_reputations:
|
| 45 | # New agent: base weight 1.0
|
| 46 | self.agent_reputations[agent_id] = 1.0
|
| 47 | return self.agent_reputations[agent_id]
|
| 48 |
|
| 49 | def set_reputation(self, agent_id: str, weight: float):
|
| 50 | """Manually set reputation for demo purposes"""
|
| 51 | self.agent_reputations[agent_id] = weight
|
| 52 |
|
| 53 | def propose(
|
| 54 | self,
|
| 55 | title: str,
|
| 56 | description: str,
|
| 57 | code_diff: str,
|
| 58 | proposer: str,
|
| 59 | threshold_type: str = "supermajority",
|
| 60 | threshold_value: float = 0.67
|
| 61 | ) -> Proposal:
|
| 62 | """Create a new proposal"""
|
| 63 | proposal_id = hashlib.sha256(
|
| 64 | f"{title}{proposer}{time.time()}".encode()
|
| 65 | ).hexdigest()[:16]
|
| 66 |
|
| 67 | proposal = Proposal(
|
| 68 | id=proposal_id,
|
| 69 | title=title,
|
| 70 | description=description,
|
| 71 | code_diff=code_diff,
|
| 72 | proposer=proposer,
|
| 73 | created_at=datetime.utcnow().isoformat(),
|
| 74 | status="pending",
|
| 75 | threshold_type=threshold_type,
|
| 76 | threshold_value=threshold_value
|
| 77 | )
|
| 78 |
|
| 79 | self.proposals[proposal_id] = proposal
|
| 80 | self.votes[proposal_id] = []
|
| 81 |
|
| 82 | print(f"\nā
Proposal created: {proposal_id}")
|
| 83 | print(f" Title: {title}")
|
| 84 | print(f" Threshold: {threshold_value*100}% {threshold_type}")
|
| 85 |
|
| 86 | return proposal
|
| 87 |
|
| 88 | def vote(
|
| 89 | self,
|
| 90 | proposal_id: str,
|
| 91 | vote: str,
|
| 92 | voter: str,
|
| 93 | signature: str = "demo_sig",
|
| 94 | auto_finalize: bool = False
|
| 95 | ) -> bool:
|
| 96 | """Cast a vote on a proposal"""
|
| 97 | if proposal_id not in self.proposals:
|
| 98 | raise ValueError(f"Proposal {proposal_id} not found")
|
| 99 |
|
| 100 | if self.proposals[proposal_id].status != "pending":
|
| 101 | print(f"ā ļø {voter} attempted to vote on finalized proposal")
|
| 102 | return False
|
| 103 |
|
| 104 | # Check if already voted
|
| 105 | existing_votes = [v for v in self.votes[proposal_id] if v.voter == voter]
|
| 106 | if existing_votes:
|
| 107 | raise ValueError(f"{voter} has already voted on this proposal")
|
| 108 |
|
| 109 | weight = self.calculate_reputation(voter)
|
| 110 |
|
| 111 | vote_obj = Vote(
|
| 112 | proposal_id=proposal_id,
|
| 113 | voter=voter,
|
| 114 | vote=vote,
|
| 115 | weight=weight,
|
| 116 | timestamp=datetime.utcnow().isoformat(),
|
| 117 | signature=signature
|
| 118 | )
|
| 119 |
|
| 120 | self.votes[proposal_id].append(vote_obj)
|
| 121 |
|
| 122 | emoji = "ā
" if vote == "approve" else "ā" if vote == "reject" else "āŖ"
|
| 123 | print(f"{emoji} {voter} voted {vote.upper()} (weight: {weight:.1f})")
|
| 124 |
|
| 125 | # Check if threshold reached (only finalize if auto_finalize is True)
|
| 126 | if auto_finalize:
|
| 127 | self.check_threshold(proposal_id)
|
| 128 |
|
| 129 | return True
|
| 130 |
|
| 131 | def check_threshold(self, proposal_id: str) -> bool:
|
| 132 | """Check if proposal has reached consensus threshold"""
|
| 133 | proposal = self.proposals[proposal_id]
|
| 134 | votes = self.votes[proposal_id]
|
| 135 |
|
| 136 | if not votes:
|
| 137 | return False
|
| 138 |
|
| 139 | total_weight = sum(v.weight for v in votes)
|
| 140 | approve_weight = sum(v.weight for v in votes if v.vote == "approve")
|
| 141 | reject_weight = sum(v.weight for v in votes if v.vote == "reject")
|
| 142 |
|
| 143 | approve_ratio = approve_weight / total_weight if total_weight > 0 else 0
|
| 144 |
|
| 145 | print(f"\nš Current tally: {approve_weight:.1f} approve / {total_weight:.1f} total ({approve_ratio*100:.1f}%)")
|
| 146 |
|
| 147 | if approve_ratio >= proposal.threshold_value:
|
| 148 | self.proposals[proposal_id].status = "approved"
|
| 149 | print(f"\nš CONSENSUS REACHED! Proposal {proposal_id} APPROVED")
|
| 150 | print(f" {approve_weight:.1f} / {total_weight:.1f} votes ({approve_ratio*100:.1f}% ā„ {proposal.threshold_value*100}%)")
|
| 151 | return True
|
| 152 |
|
| 153 | # Check if rejection is impossible to overcome
|
| 154 | if reject_weight > total_weight * (1 - proposal.threshold_value):
|
| 155 | self.proposals[proposal_id].status = "rejected"
|
| 156 | print(f"\nā Proposal {proposal_id} REJECTED")
|
| 157 | print(f" Not enough approve votes to reach threshold")
|
| 158 | return False
|
| 159 |
|
| 160 | return False
|
| 161 |
|
| 162 | def get_proposal(self, proposal_id: str) -> Optional[Proposal]:
|
| 163 | """Get proposal by ID"""
|
| 164 | return self.proposals.get(proposal_id)
|
| 165 |
|
| 166 | def get_votes(self, proposal_id: str) -> List[Vote]:
|
| 167 | """Get all votes for a proposal"""
|
| 168 | return self.votes.get(proposal_id, [])
|
| 169 |
|
| 170 | def export_consensus_proof(self, proposal_id: str) -> dict:
|
| 171 | """Export cryptographic proof of consensus for Git commit"""
|
| 172 | proposal = self.proposals[proposal_id]
|
| 173 | votes = self.votes[proposal_id]
|
| 174 |
|
| 175 | return {
|
| 176 | "proposal": asdict(proposal),
|
| 177 | "votes": [asdict(v) for v in votes],
|
| 178 | "consensus": {
|
| 179 | "reached": proposal.status == "approved",
|
| 180 | "threshold": proposal.threshold_value,
|
| 181 | "approve_weight": sum(v.weight for v in votes if v.vote == "approve"),
|
| 182 | "total_weight": sum(v.weight for v in votes),
|
| 183 | "timestamp": datetime.utcnow().isoformat()
|
| 184 | }
|
| 185 | }
|
| 186 |
|
| 187 | # Byzantine Fault Tolerance utilities
|
| 188 | class BFTValidator:
|
| 189 | """Validate that consensus meets BFT safety guarantees"""
|
| 190 |
|
| 191 | @staticmethod
|
| 192 | def min_agents_for_safety(max_faulty: int) -> int:
|
| 193 | """Calculate minimum agents needed: n ā„ 3f + 1"""
|
| 194 | return 3 * max_faulty + 1
|
| 195 |
|
| 196 | @staticmethod
|
| 197 | def max_faulty_tolerated(total_agents: int) -> int:
|
| 198 | """Calculate max faulty agents tolerated: f = ā(n-1)/3ā"""
|
| 199 | return (total_agents - 1) // 3
|
| 200 |
|
| 201 | @staticmethod
|
| 202 | def is_safe_configuration(total_agents: int, max_faulty: int) -> bool:
|
| 203 | """Check if agent count satisfies BFT safety"""
|
| 204 | return total_agents >= BFTValidator.min_agents_for_safety(max_faulty)
|
| 205 |
|
| 206 |
|