From bcc2c14b3a2744259b625ca0b8e1377e2abdf3b5 Mon Sep 17 00:00:00 2001 From: AddNad Samuel Date: Tue, 7 Apr 2026 10:22:52 +0100 Subject: [PATCH] feat: Implement TEE reputation system (#49) Add comprehensive reputation contract for tracking and scoring TEE performance. Features: - Track per-TEE statistics: total requests, successes, failures - Record downtime windows with audit trail - Calculate reputation scores using weighted formula * Success rate: 50% weight * Uptime ratio: 35% weight (calculates from downtime) * Response quality: 15% weight - Four reputation tiers: poor, fair, good, excellent - Client query interface to find top TEEs by reputation - Admin controls for configurable weights and thresholds Integration: - Off-chain indexer listens to InferenceSettlementRelay events - Calls recordSettlement() with success/failure status - Heartbeat monitor tracks TEE downtime via TEERegistry - Supports downtime reasons: heartbeat_missed, pcr_revoked, manual_disable Contract includes: - Full role-based access control (recorder, monitor, admin) - Comprehensive test suite with 15+ test cases - Detailed integration guide for deployment and usage Resolves #49 --- contracts/solidity/TEEReputation.sol | 543 +++++++++++++++++++ contracts/solidity/TEE_REPUTATION_GUIDE.md | 364 +++++++++++++ contracts/solidity/tests/TEEReputation.t.sol | 325 +++++++++++ 3 files changed, 1232 insertions(+) create mode 100644 contracts/solidity/TEEReputation.sol create mode 100644 contracts/solidity/TEE_REPUTATION_GUIDE.md create mode 100644 contracts/solidity/tests/TEEReputation.t.sol diff --git a/contracts/solidity/TEEReputation.sol b/contracts/solidity/TEEReputation.sol new file mode 100644 index 00000000..6d5e9508 --- /dev/null +++ b/contracts/solidity/TEEReputation.sol @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/access/AccessControl.sol"; +import "./TEERegistry.sol"; + +/** + * @title TEEReputation - Track and Score TEE Performance + * @notice On-chain reputation system that tracks TEE performance metrics + * (successful/failed requests, downtime, response times) and provides + * reputation scores to help clients select the best TEEs. + * + * @dev ## Design Overview + * + * The reputation contract aggregates performance data from two sources: + * 1. **InferenceSettlementRelay** events — track successful/failed requests + * 2. **TEERegistry** state changes — track downtime, PCR revocations, enable/disable + * + * Reputation scores are calculated using a weighted formula: + * - Success rate: 50% weight + * - Uptime ratio: 35% weight + * - Response quality: 15% weight + * + * Scores range from 0-10000 (fixed-point, divide by 100 for percentage). + * Clients can query TEEs by reputation tier (poor, fair, good, excellent). + * + * ## Integration Flow + * + * 1. **Off-chain indexer** listens to InferenceSettlementRelay.IndividualSettlement + * 2. Calls recordSettlement(teeId, successful, responseTimeMs) + * 3. Contract updates stats and recalculates reputation + * 4. Client queries getTopTEEsByReputation(teeType, minTier, limit) + * 5. Downtime is tracked via recordDowntime() from heartbeat monitoring + * + * ## Downtime Tracking + * + * Downtime windows are recorded for: + * - Heartbeat miss: TEE fails to send liveness proof + * - PCR revocation: Enclave code becomes compromised + * - Manual disable: Owner disables TEE + * + * Downtime is resolved when: + * - TEE sends fresh heartbeat (if was heartbeat miss) + * - TEE is re-enabled after owner/admin action + */ +contract TEEReputation is AccessControl { + // ============ Constants ============ + + bytes32 public constant SETTLEMENT_RECORDER_ROLE = keccak256("SETTLEMENT_RECORDER"); + bytes32 public constant HEARTBEAT_MONITOR_ROLE = keccak256("HEARTBEAT_MONITOR"); + + // Reputation tiers + uint8 public constant TIER_POOR = 0; + uint8 public constant TIER_FAIR = 1; + uint8 public constant TIER_GOOD = 2; + uint8 public constant TIER_EXCELLENT = 3; + + // Max reputation value (fixed-point: 10000 = 100%) + uint256 public constant MAX_REPUTATION = 10000; + + // ============ Structs ============ + + /// @notice Per-TEE performance statistics + struct TEEStats { + // Request tracking + uint64 totalRequests; // total inference requests attributed + uint64 successfulRequests; // completed successfully + uint64 failedRequests; // failed/reverted + + // Reliability metrics + uint32 totalDowntimeSeconds; // aggregated offline duration + uint256 lastDowntimeStart; // if currently down, when it started + bool isCurrentlyDown; // quick check flag + + // Performance + uint256 averageResponseTime; // milliseconds (tracked off-chain, stored here) + + // Timestamps + uint256 firstRequestAt; // block timestamp of first request + uint256 lastRequestAt; // block timestamp of most recent request + uint256 lastUpdatedAt; // block timestamp of last stats update + } + + /// @notice Reputation score with calculation metadata + struct ReputationScore { + uint256 score; // 0-10000 (fixed-point) + uint256 calculatedAt; // block timestamp of calculation + uint8 tier; // 0=poor, 1=fair, 2=good, 3=excellent + } + + /// @notice Downtime window record for auditing + struct DowntimeWindow { + uint256 startTimestamp; // when downtime started + uint256 endTimestamp; // when downtime ended (0 if ongoing) + string reason; // "heartbeat_missed", "pcr_revoked", "manual_disable" + } + + // ============ Storage ============ + + TEERegistry public immutable REGISTRY; + + // TEE statistics: teeId => TEEStats + mapping(bytes32 => TEEStats) public stats; + + // Downtime history: teeId => DowntimeWindow array + mapping(bytes32 => DowntimeWindow[]) public downtimeHistory; + + // Cached reputation scores: teeId => ReputationScore + mapping(bytes32 => ReputationScore) public cachedReputation; + + // Current tracked downtime period: teeId => downtime window index (-1 if none) + mapping(bytes32 => int256) public currentDowntimeIndex; + + // ============ Configuration (tunable via admin) ============ + + /// @notice Success rate weight in basis points (default 5000 = 50%) + uint256 public successWeightBps = 5000; + + /// @notice Uptime ratio weight in basis points (default 3500 = 35%) + uint256 public uptimeWeightBps = 3500; + + /// @notice Response quality weight in basis points (default 1500 = 15%) + uint256 public responseWeightBps = 1500; + + /// @notice Reputation tier thresholds + uint256 public tierExcellentThreshold = 9000; // >= 9000 + uint256 public tierGoodThreshold = 7000; // >= 7000 + uint256 public tierFairThreshold = 4000; // >= 4000 + // Below 4000 = poor + + // ============ Events ============ + + event SettlementRecorded( + bytes32 indexed teeId, + bool successful, + uint256 responseTimeMs, + uint256 indexed blockNumber + ); + + event DowntimeRecorded( + bytes32 indexed teeId, + uint256 durationSeconds, + string reason, + uint256 indexed blockNumber + ); + + event DowntimeResolved( + bytes32 indexed teeId, + uint256 totalDowntimeSeconds, + uint256 indexed blockNumber + ); + + event ReputationUpdated( + bytes32 indexed teeId, + uint256 score, + uint8 tier, + uint256 indexed blockNumber + ); + + event WeightsUpdated( + uint256 successWeightBps, + uint256 uptimeWeightBps, + uint256 responseWeightBps + ); + + event TierThresholdsUpdated( + uint256 excellentThreshold, + uint256 goodThreshold, + uint256 fairThreshold + ); + + // ============ Constructor ============ + + constructor(address _registry) { + require(_registry != address(0), "Invalid registry address"); + REGISTRY = TEERegistry(_registry); + + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _grantRole(SETTLEMENT_RECORDER_ROLE, msg.sender); + _grantRole(HEARTBEAT_MONITOR_ROLE, msg.sender); + } + + // ============ Settlement Recording ============ + + /** + * @notice Record a settled inference request for a TEE + * @dev Called by off-chain indexer listening to InferenceSettlementRelay events + * @param teeId The TEE's unique identifier + * @param successful Whether the request completed successfully + * @param responseTimeMs Response time in milliseconds + */ + function recordSettlement( + bytes32 teeId, + bool successful, + uint256 responseTimeMs + ) external onlyRole(SETTLEMENT_RECORDER_ROLE) { + require(teeId != bytes32(0), "Invalid teeId"); + + TEEStats storage teeStats = stats[teeId]; + + // Initialize first request timestamp + if (teeStats.totalRequests == 0) { + teeStats.firstRequestAt = block.timestamp; + } + + // Update counters + teeStats.totalRequests++; + if (successful) { + teeStats.successfulRequests++; + } else { + teeStats.failedRequests++; + } + + // Update response time (simple moving average) + if (teeStats.averageResponseTime == 0) { + teeStats.averageResponseTime = responseTimeMs; + } else { + teeStats.averageResponseTime = + (teeStats.averageResponseTime * 9 + responseTimeMs * 1) / 10; + } + + teeStats.lastRequestAt = block.timestamp; + teeStats.lastUpdatedAt = block.timestamp; + + // Recalculate reputation + _updateReputationScore(teeId); + + emit SettlementRecorded(teeId, successful, responseTimeMs, block.number); + } + + // ============ Downtime Tracking ============ + + /** + * @notice Record a downtime window for a TEE + * @dev Called by heartbeat monitor service + * @param teeId The TEE's unique identifier + * @param durationSeconds Duration of downtime in seconds (0 = ongoing) + * @param reason Why the TEE went down + */ + function recordDowntime( + bytes32 teeId, + uint256 durationSeconds, + string calldata reason + ) external onlyRole(HEARTBEAT_MONITOR_ROLE) { + require(teeId != bytes32(0), "Invalid teeId"); + require(bytes(reason).length > 0, "Reason cannot be empty"); + + TEEStats storage teeStats = stats[teeId]; + + // If TEE is not already down, start a new downtime window + if (currentDowntimeIndex[teeId] == -1) { + // Create new downtime window + uint256 endTime = durationSeconds == 0 ? 0 : block.timestamp + durationSeconds; + downtimeHistory[teeId].push( + DowntimeWindow({ + startTimestamp: block.timestamp, + endTimestamp: endTime, + reason: reason + }) + ); + + // Update index + currentDowntimeIndex[teeId] = int256(downtimeHistory[teeId].length) - 1; + teeStats.isCurrentlyDown = true; + teeStats.lastDowntimeStart = block.timestamp; + + emit DowntimeRecorded(teeId, durationSeconds, reason, block.number); + } + } + + /** + * @notice Resolve ongoing downtime for a TEE + * @dev Called when TEE recovers (fresh heartbeat, re-enabled, etc.) + * @param teeId The TEE's unique identifier + */ + function resolveDowntime(bytes32 teeId) external onlyRole(HEARTBEAT_MONITOR_ROLE) { + require(teeId != bytes32(0), "Invalid teeId"); + + TEEStats storage teeStats = stats[teeId]; + int256 currentIdx = currentDowntimeIndex[teeId]; + + if (currentIdx >= 0) { + DowntimeWindow storage window = downtimeHistory[teeId][uint256(currentIdx)]; + + // Only resolve if this window is still ongoing + if (window.endTimestamp == 0) { + window.endTimestamp = block.timestamp; + + // Calculate downtime duration and add to total + uint256 durationSeconds = window.endTimestamp - window.startTimestamp; + teeStats.totalDowntimeSeconds += uint32(durationSeconds); + + // Clear flags + teeStats.isCurrentlyDown = false; + teeStats.lastDowntimeStart = 0; + currentDowntimeIndex[teeId] = -1; + + // Recalculate reputation + _updateReputationScore(teeId); + + emit DowntimeResolved(teeId, teeStats.totalDowntimeSeconds, block.number); + } + } + } + + // ============ Reputation Calculation ============ + + /** + * @notice Calculate reputation score for a TEE + * @dev Uses weighted formula: (success * 50% + uptime * 35% + response * 15%) + * @param teeId The TEE's unique identifier + * @return score Reputation score (0-10000, where 10000 = 100%) + */ + function calculateReputationScore(bytes32 teeId) public view returns (ReputationScore memory) { + TEEStats storage teeStats = stats[teeId]; + + // If no requests yet, return neutral score + if (teeStats.totalRequests == 0) { + return ReputationScore({score: 5000, calculatedAt: block.timestamp, tier: TIER_FAIR}); + } + + // Calculate success rate (0-10000) + uint256 successRate = (teeStats.successfulRequests * MAX_REPUTATION) / teeStats.totalRequests; + + // Calculate uptime ratio (0-10000) + uint256 totalTime = block.timestamp - teeStats.firstRequestAt; + uint256 uptimeSeconds = totalTime - teeStats.totalDowntimeSeconds; + uint256 uptimeRatio = (uptimeSeconds * MAX_REPUTATION) / (totalTime > 0 ? totalTime : 1); + + // Calculate response quality (0-10000, where lower ms = higher score) + // Normalize: 1000ms = 10000 points, 5000ms = 5000 points, etc. + uint256 responseQuality; + if (teeStats.averageResponseTime == 0) { + responseQuality = MAX_REPUTATION; // Perfect if no data + } else { + responseQuality = + MAX_REPUTATION - ((teeStats.averageResponseTime * MAX_REPUTATION) / 5000); + if (responseQuality > MAX_REPUTATION) { + responseQuality = 0; // Cap at 0 if very slow + } + } + + // Weighted combination + uint256 score = + ((successRate * successWeightBps) + (uptimeRatio * uptimeWeightBps) + + (responseQuality * responseWeightBps)) / 10000; + + // Ensure score is within bounds + if (score > MAX_REPUTATION) { + score = MAX_REPUTATION; + } + + // Determine tier + uint8 tier = TIER_POOR; + if (score >= tierExcellentThreshold) { + tier = TIER_EXCELLENT; + } else if (score >= tierGoodThreshold) { + tier = TIER_GOOD; + } else if (score >= tierFairThreshold) { + tier = TIER_FAIR; + } + + return ReputationScore({score: score, calculatedAt: block.timestamp, tier: tier}); + } + + /** + * @notice Internal function to update cached reputation score + * @param teeId The TEE's unique identifier + */ + function _updateReputationScore(bytes32 teeId) internal { + ReputationScore memory newScore = calculateReputationScore(teeId); + cachedReputation[teeId] = newScore; + emit ReputationUpdated(teeId, newScore.score, newScore.tier, block.number); + } + + // ============ Query Functions ============ + + /** + * @notice Get top TEEs by reputation for a given type + * @param teeType TEE type to filter by + * @param minTierRequired Minimum reputation tier required (0=poor, 1=fair, 2=good, 3=excellent) + * @param limit Maximum number of TEEs to return + * @return topTEEs Array of TEE IDs sorted by reputation (highest first) + * @return scores Array of corresponding reputation scores + */ + function getTopTEEsByReputation( + uint8 teeType, + uint8 minTierRequired, + uint256 limit + ) external view returns (bytes32[] memory topTEEs, ReputationScore[] memory scores) { + // Get enabled TEEs of the type from registry + bytes32[] memory enabledTEEs = REGISTRY.getEnabledTEEs(teeType); + + if (enabledTEEs.length == 0) { + return (new bytes32[](0), new ReputationScore[](0)); + } + + // Calculate scores for all, filter by tier, and sort + ReputationScore[] memory allScores = new ReputationScore[](enabledTEEs.length); + uint256 validCount = 0; + + for (uint256 i = 0; i < enabledTEEs.length; i++) { + ReputationScore memory score = calculateReputationScore(enabledTEEs[i]); + if (score.tier >= minTierRequired) { + allScores[i] = score; + validCount++; + } + } + + if (validCount == 0) { + return (new bytes32[](0), new ReputationScore[](0)); + } + + // Limit the result set + uint256 returnCount = validCount < limit ? validCount : limit; + bytes32[] memory result = new bytes32[](returnCount); + ReputationScore[] memory resultScores = new ReputationScore[](returnCount); + + // Simple selection sort to get top N (not optimal, but clear) + bytes32[] memory sortedTEEs = enabledTEEs; + uint256 resultIdx = 0; + + for (uint256 i = 0; i < returnCount && resultIdx < returnCount; i++) { + uint256 maxScore = 0; + uint256 maxIdx = type(uint256).max; + + // Find next highest score + for (uint256 j = 0; j < sortedTEEs.length; j++) { + if (sortedTEEs[j] != bytes32(0)) { + ReputationScore memory score = calculateReputationScore(sortedTEEs[j]); + if (score.tier >= minTierRequired && score.score > maxScore) { + maxScore = score.score; + maxIdx = j; + } + } + } + + if (maxIdx < type(uint256).max) { + result[resultIdx] = sortedTEEs[maxIdx]; + resultScores[resultIdx] = calculateReputationScore(sortedTEEs[maxIdx]); + sortedTEEs[maxIdx] = bytes32(0); // Mark as used + resultIdx++; + } + } + + return (result, resultScores); + } + + /** + * @notice Get statistics for a TEE + * @param teeId The TEE's unique identifier + * @return stats The TEE's statistics + */ + function getTEEStats(bytes32 teeId) external view returns (TEEStats memory) { + return stats[teeId]; + } + + /** + * @notice Get downtime history for a TEE + * @param teeId The TEE's unique identifier + * @return Array of downtime windows + */ + function getDowntimeHistory(bytes32 teeId) + external + view + returns (DowntimeWindow[] memory) + { + return downtimeHistory[teeId]; + } + + /** + * @notice Get cached reputation score for a TEE + * @param teeId The TEE's unique identifier + * @return The cached reputation score + */ + function getReputationScore(bytes32 teeId) external view returns (ReputationScore memory) { + return cachedReputation[teeId]; + } + + // ============ Admin Functions ============ + + /** + * @notice Update scoring weights + * @dev Weights must sum to 10000 (100%) + * @param newSuccessWeightBps New weight for success rate + * @param newUptimeWeightBps New weight for uptime + * @param newResponseWeightBps New weight for response quality + */ + function updateWeights( + uint256 newSuccessWeightBps, + uint256 newUptimeWeightBps, + uint256 newResponseWeightBps + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require( + newSuccessWeightBps + newUptimeWeightBps + newResponseWeightBps == 10000, + "Weights must sum to 10000" + ); + + successWeightBps = newSuccessWeightBps; + uptimeWeightBps = newUptimeWeightBps; + responseWeightBps = newResponseWeightBps; + + emit WeightsUpdated(newSuccessWeightBps, newUptimeWeightBps, newResponseWeightBps); + } + + /** + * @notice Update reputation tier thresholds + * @param newExcellentThreshold Score threshold for "excellent" tier + * @param newGoodThreshold Score threshold for "good" tier + * @param newFairThreshold Score threshold for "fair" tier + */ + function updateTierThresholds( + uint256 newExcellentThreshold, + uint256 newGoodThreshold, + uint256 newFairThreshold + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require( + newExcellentThreshold >= newGoodThreshold && + newGoodThreshold >= newFairThreshold && + newFairThreshold > 0 && + newExcellentThreshold <= MAX_REPUTATION, + "Invalid thresholds" + ); + + tierExcellentThreshold = newExcellentThreshold; + tierGoodThreshold = newGoodThreshold; + tierFairThreshold = newFairThreshold; + + emit TierThresholdsUpdated(newExcellentThreshold, newGoodThreshold, newFairThreshold); + } + + /** + * @notice Manually recalculate reputation for a TEE + * @dev Useful if weights or thresholds have changed + * @param teeId The TEE's unique identifier + */ + function recalculateReputation(bytes32 teeId) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + _updateReputationScore(teeId); + } +} diff --git a/contracts/solidity/TEE_REPUTATION_GUIDE.md b/contracts/solidity/TEE_REPUTATION_GUIDE.md new file mode 100644 index 00000000..9a975bdd --- /dev/null +++ b/contracts/solidity/TEE_REPUTATION_GUIDE.md @@ -0,0 +1,364 @@ +# TEE Reputation System - Implementation Guide + +## Overview + +The **TEEReputation** contract implements a reputation tracking system for Trusted Execution Environment (TEE) nodes in OpenGradient. It monitors TEE performance metrics and provides reputation scores to help clients select the best TEEs for inference requests. + +**Status**: ✅ Implements issue #49 - "Add reputation contract for TEEs" + +## Architecture + +``` +InferenceSettlementRelay (existing) + ↓ emits IndividualSettlement events + ↓ (off-chain indexer listens) +TEEReputation.recordSettlement() + ↓ +Stats Updated → Reputation Recalculated + ↓ +Client queries getTopTEEsByReputation() + +TEERegistry (existing) + ↓ monitors heartbeat/enable/disable + ↓ (off-chain service listens) +TEEReputation.recordDowntime() / resolveDowntime() + ↓ +Downtime tracked → Reputation affected +``` + +## Key Features + +### 1. **Performance Tracking** +- Total requests per TEE +- Successful vs. failed requests +- Average response time (moving average) +- Success rate calculation + +### 2. **Downtime Monitoring** +- Records downtime windows with reasons: + - `heartbeat_missed` - TEE failed to send periodic liveness proof + - `pcr_revoked` - Enclave code became compromised + - `manual_disable` - Owner/admin disabled TEE +- Tracks total downtime per TEE +- Audit trail of all downtime periods + +### 3. **Reputation Scoring** +``` +Reputation = ( + SuccessRate (50%) + + UptimeRatio (35%) + + ResponseQuality (15%) +) × 100% + +Score: 0-10000 (10000 = 100%) +``` + +- **Tiers**: Poor (0-3999), Fair (4000-6999), Good (7000-8999), Excellent (9000+) +- **Configurable weights** via admin panel +- **Cached scores** for gas efficiency + +### 4. **Client Query Interface** +```solidity +// Get top 10 "excellent" LLM inference TEEs +(bytes32[] memory tees, ReputationScore[] memory scores) = + reputation.getTopTEEsByReputation( + 1, // teeType: LLM + 3, // minTier: EXCELLENT + 10 // limit + ); +``` + +## Integration Steps + +### Step 1: Deploy the Contract + +```bash +# In og-evm/contracts/solidity/ +npx hardhat compile + +# Deploy +npx hardhat run scripts/deploy-reputation.js --network +``` + +**Constructor Requirements:** +```solidity +TEEReputation reputation = new TEEReputation( + address(teeRegistry) // existing TEERegistry contract address +); +``` + +### Step 2: Grant Roles + +```solidity +// Admin account does: + +// Grant settlement recording role to off-chain indexer service +reputation.grantRole( + reputation.SETTLEMENT_RECORDER_ROLE(), + INDEXER_SERVICE_ADDRESS +); + +// Grant downtime monitoring role to heartbeat monitor service +reputation.grantRole( + reputation.HEARTBEAT_MONITOR_ROLE(), + HEARTBEAT_MONITOR_ADDRESS +); +``` + +### Step 3: Set Up Off-Chain Indexer + +Create a service that listens to `InferenceSettlementRelay.IndividualSettlement` events: + +```javascript +// Pseudo-code: off-chain indexer +const contract = new ethers.Contract( + INFERENCE_SETTLEMENT_RELAY_ADDRESS, + INFERENCE_SETTLEMENT_RELAY_ABI, + provider +); + +contract.on('IndividualSettlement', async (teeId, ethAddress, inputHash, + outputHash, timestamp, walrusBlobId, signature) => { + + // Query Walrus/IPFS to check if inference was successful + const result = await queryResult(walrusBlobId); + const successful = result.status === 'success'; + const responseTimeMs = result.responseTime || 1000; + + // Record on-chain + const tx = await reputationContract.recordSettlement( + teeId, + successful, + responseTimeMs, + { gasPrice: ethers.utils.parseUnits('1', 'gwei') } + ); + + console.log(`Recorded settlement for TEE ${teeId}: ${tx.hash}`); +}); +``` + +### Step 4: Set Up Downtime Monitor + +Create a service that monitors heartbeat freshness and TEE status: + +```javascript +// Pseudo-code: heartbeat monitor +async function monitorHeartbeats() { + const teeRegistry = new ethers.Contract(...); + const reputation = new ethers.Contract(...); + + setInterval(async () => { + const allTEEs = await teeRegistry.getAllTEEs(); + const maxHeartbeatAge = 1800; // 30 minutes (from TEERegistry) + + for (const tee of allTEEs) { + const teeData = await teeRegistry.getTEE(tee.id); + const secondsSinceHeartbeat = Math.floor(Date.now() / 1000) - + teeData.lastHeartbeatAt.toNumber(); + + if (secondsSinceHeartbeat > maxHeartbeatAge) { + // TEE is down due to missed heartbeat + const stats = await reputation.stats(tee.id); + + if (!stats.isCurrentlyDown) { + // Start downtime tracking + await reputation.recordDowntime( + tee.id, + 0, // duration 0 = ongoing + "heartbeat_missed" + ); + } + } else if (secondsSinceHeartbeat <= maxHeartbeatAge) { + // TEE recovered + const stats = await reputation.stats(tee.id); + + if (stats.isCurrentlyDown) { + // Resolve downtime + await reputation.resolveDowntime(tee.id); + } + } + } + }, 60000); // Check every 60 seconds +} +``` + +## Usage Examples + +### For Clients: Find Best TEEs + +```solidity +// Get top 5 "good" or better LLM TEEs +(bytes32[] memory topTEEs, ) = reputation.getTopTEEsByReputation( + 1, // LLM type + 2, // GOOD tier minimum + 5 // Return top 5 +); + +for (uint256 i = 0; i < topTEEs.length; i++) { + bytes32 teeId = topTEEs[i]; + // Connect to TEE endpoint and verify TLS certificate + makeInferenceRequest(teeId); +} +``` + +### For Monitoring: Check TEE Status + +```solidity +// Get reputation score +TEEReputation.ReputationScore memory score = + reputation.cachedReputation(teeId); + +console.log("Score:", score.score, "/10000"); +console.log("Tier:", score.tier); // 0=poor, 1=fair, 2=good, 3=excellent + +// Get detailed stats +TEEReputation.TEEStats memory stats = reputation.stats(teeId); + +console.log("Success rate:", + (stats.successfulRequests * 100) / stats.totalRequests, "%"); +console.log("Total downtime:", stats.totalDowntimeSeconds, "seconds"); +console.log("Currently down:", stats.isCurrentlyDown); +``` + +### For Administration: Configure Weights + +```solidity +// Adjust scoring formula if needed +// Example: emphasize availability over response time + +reputation.updateWeights( + 5000, // success rate: 50% (unchanged) + 4500, // uptime ratio: 45% (increased from 35%) + 500 // response quality: 5% (decreased from 15%) +); + +// Update tier thresholds if needed +reputation.updateTierThresholds( + 9500, // excellent: >= 95% + 8000, // good: >= 80% + 5000 // fair: >= 50% +); +``` + +## Data Structures + +### TEEStats +```solidity +struct TEEStats { + uint64 totalRequests; // Total inference requests attributed + uint64 successfulRequests; // Completed successfully + uint64 failedRequests; // Failed/reverted + + uint32 totalDowntimeSeconds; // Total offline duration + uint256 lastDowntimeStart; // Current downtime start (if down) + bool isCurrentlyDown; // Quick status check + + uint256 averageResponseTime; // Milliseconds (moving average) + + uint256 firstRequestAt; // Timestamp of first request + uint256 lastRequestAt; // Timestamp of most recent request + uint256 lastUpdatedAt; // Timestamp of last stats update +} +``` + +### ReputationScore +```solidity +struct ReputationScore { + uint256 score; // 0-10000 (fixed-point) + uint256 calculatedAt; // Block timestamp + uint8 tier; // 0=poor, 1=fair, 2=good, 3=excellent +} +``` + +### DowntimeWindow (Audit Trail) +```solidity +struct DowntimeWindow { + uint256 startTimestamp; // When downtime started + uint256 endTimestamp; // When downtime ended (0 if ongoing) + string reason; // "heartbeat_missed" | "pcr_revoked" | "manual_disable" +} +``` + +## Role-Based Access Control + +| Role | Permissions | Typical User | +|------|-------------|--------------| +| `DEFAULT_ADMIN_ROLE` | Update weights, thresholds, recalculate reputation | Admin/DAO | +| `SETTLEMENT_RECORDER_ROLE` | Call `recordSettlement()` | Off-chain indexer service | +| `HEARTBEAT_MONITOR_ROLE` | Call `recordDowntime()` / `resolveDowntime()` | Heartbeat monitor service | + +## Gas Optimization Considerations + +1. **Cached Reputation**: Scores are cached to avoid recalculation on every query + - Recalculated when: settlements recorded, downtime resolved, weights changed + - Query cached score via `cachedReputation[teeId]` + +2. **Batch Operations**: For off-chain indexers recording many settlements: + ```javascript + // More gas-efficient than individual calls + const txs = await Promise.all( + settlements.map(s => + reputation.recordSettlement(s.teeId, s.success, s.responseTime) + ) + ); + ``` + +3. **Downtime Efficiency**: Downtime windows stored in array (unbounded) for auditability + - Consider pagination on front-end if TEE has very long history + +## Testing + +Run the comprehensive test suite: + +```bash +cd contracts/solidity +npx hardhat test tests/TEEReputation.t.sol + +# For Foundry tests (if using Foundry): +forge test --match-path contracts/solidity/tests/TEEReputation.t.sol +``` + +Test coverage includes: +- ✅ Settlement recording (success/fail, multiple, response time) +- ✅ Downtime tracking (record, resolve, multiple windows) +- ✅ Reputation calculation (various scenarios) +- ✅ Configuration (weights, thresholds) +- ✅ Access control (role-based restrictions) +- ✅ Integration (end-to-end flow) + +## Future Enhancements + +1. **Geographic Distribution**: Track TEE location for latency-aware selection +2. **Specialized Metrics**: + - Per-model-type success rates + - Per-client success rates (for privacy-respecting recommendations) +3. **Reputation Decay**: Reduce weight of old data over time +4. **Slashing**: If TEE fails enough times, automatically blacklist +5. **Incentive Mechanism**: Reward high-reputation TEEs with priority task assignment + +## Deployment Checklist + +- [ ] Deploy TEEReputation contract (pass TEERegistry address) +- [ ] Grant SETTLEMENT_RECORDER_ROLE to indexer service +- [ ] Grant HEARTBEAT_MONITOR_ROLE to heartbeat monitor +- [ ] Start off-chain indexer listening to InferenceSettlementRelay events +- [ ] Start heartbeat monitor service +- [ ] Configure initial weights/thresholds if needed +- [ ] Verify first settlements are being recorded +- [ ] Verify reputation scores are calculating correctly +- [ ] Update client SDKs to use `getTopTEEsByReputation()` for selection +- [ ] Monitor through test period before production + +## Support + +For questions or issues: +- See TEERegistry documentation for chain of trust details +- See InferenceSettlementRelay for settlement event structure +- Check test suite for usage examples +- Open issue in GitHub repository + +--- + +**File**: `/contracts/solidity/TEEReputation.sol` +**Tests**: `/contracts/solidity/tests/TEEReputation.t.sol` +**Related**: TEERegistry, InferenceSettlementRelay diff --git a/contracts/solidity/tests/TEEReputation.t.sol b/contracts/solidity/tests/TEEReputation.t.sol new file mode 100644 index 00000000..d35c1159 --- /dev/null +++ b/contracts/solidity/tests/TEEReputation.t.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {TEEReputation} from "../TEEReputation.sol"; +import {TEERegistry} from "../TEERegistry.sol"; + +/** + * @title TEEReputationTest + * @notice Comprehensive test suite for the TEEReputation contract + */ +contract TEEReputationTest is Test { + TEEReputation reputation; + TEERegistry registry; + + address admin = address(0x1); + address recorder = address(0x2); + address monitor = address(0x3); + + bytes32 teeId1 = keccak256("tee1"); + bytes32 teeId2 = keccak256("tee2"); + + function setUp() public { + // Mock TEERegistry (in real tests, would use actual) + // For now, just deploy reputation with a placeholder + vm.prank(admin); + reputation = new TEEReputation(address(0x1)); // Deploy with mock address + + // Grant roles + vm.prank(admin); + reputation.grantRole(reputation.SETTLEMENT_RECORDER_ROLE(), recorder); + + vm.prank(admin); + reputation.grantRole(reputation.HEARTBEAT_MONITOR_ROLE(), monitor); + } + + // ============ Settlement Recording Tests ============ + + function test_RecordSuccessfulSettlement() public { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertEq(stats.totalRequests, 1); + assertEq(stats.successfulRequests, 1); + assertEq(stats.failedRequests, 0); + assertEq(stats.averageResponseTime, 1000); + } + + function test_RecordFailedSettlement() public { + vm.prank(recorder); + reputation.recordSettlement(teeId1, false, 2000); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertEq(stats.totalRequests, 1); + assertEq(stats.successfulRequests, 0); + assertEq(stats.failedRequests, 1); + } + + function test_MultipleSettlements() public { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1200); + + vm.prank(recorder); + reputation.recordSettlement(teeId1, false, 1500); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertEq(stats.totalRequests, 3); + assertEq(stats.successfulRequests, 2); + assertEq(stats.failedRequests, 1); + } + + function test_AverageResponseTime() public { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1100); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + // Moving average: (1000 * 9 + 1100) / 10 = 1010 + assertEq(stats.averageResponseTime, 1010); + } + + // ============ Downtime Tracking Tests ============ + + function test_RecordDowntime() public { + vm.prank(monitor); + reputation.recordDowntime(teeId1, 0, "heartbeat_missed"); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertTrue(stats.isCurrentlyDown); + assertEq(stats.lastDowntimeStart, block.timestamp); + + TEEReputation.DowntimeWindow[] memory history = reputation.getDowntimeHistory(teeId1); + assertEq(history.length, 1); + assertEq(history[0].startTimestamp, block.timestamp); + assertEq(history[0].endTimestamp, 0); // Ongoing + } + + function test_ResolveDowntime() public { + vm.prank(monitor); + reputation.recordDowntime(teeId1, 0, "heartbeat_missed"); + + vm.warp(block.timestamp + 100); + + vm.prank(monitor); + reputation.resolveDowntime(teeId1); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertFalse(stats.isCurrentlyDown); + assertEq(stats.totalDowntimeSeconds, 100); + + TEEReputation.DowntimeWindow[] memory history = reputation.getDowntimeHistory(teeId1); + assertEq(history[0].endTimestamp, block.timestamp); + } + + function test_MultipleDowntimeWindows() public { + // First downtime + vm.prank(monitor); + reputation.recordDowntime(teeId1, 0, "heartbeat_missed"); + + vm.warp(block.timestamp + 50); + + vm.prank(monitor); + reputation.resolveDowntime(teeId1); + + // Second downtime + vm.warp(block.timestamp + 100); + + vm.prank(monitor); + reputation.recordDowntime(teeId1, 0, "pcr_revoked"); + + vm.warp(block.timestamp + 30); + + vm.prank(monitor); + reputation.resolveDowntime(teeId1); + + TEEReputation.TEEStats memory stats = reputation.stats(teeId1); + assertEq(stats.totalDowntimeSeconds, 80); // 50 + 30 + + TEEReputation.DowntimeWindow[] memory history = reputation.getDowntimeHistory(teeId1); + assertEq(history.length, 2); + assertEq(history[0].endTimestamp, block.timestamp - 130); + assertEq(history[1].endTimestamp, block.timestamp); + } + + // ============ Reputation Calculation Tests ============ + + function test_ReputationWithNoRequests() public { + TEEReputation.ReputationScore memory score = reputation.calculateReputationScore(teeId1); + assertEq(score.score, 5000); // Neutral score + assertEq(score.tier, reputation.TIER_FAIR()); + } + + function test_ReputationWithPerfectSuccessRate() public { + // Record 10 successful requests + for (uint256 i = 0; i < 10; i++) { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + } + + TEEReputation.ReputationScore memory score = reputation.calculateReputationScore(teeId1); + // Success: 10000, Uptime: 10000, Response: depends on time passed + // Score should be very high but let's check it's at least good + assertGe(score.score, reputation.tierGoodThreshold()); + } + + function test_ReputationAfterDowntime() public { + // 10 successful requests + for (uint256 i = 0; i < 10; i++) { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + } + + // Record downtime + vm.prank(monitor); + reputation.recordDowntime(teeId1, 0, "heartbeat_missed"); + + // Simulate time passing + vm.warp(block.timestamp + 1000); + + // Resolve downtime + vm.prank(monitor); + reputation.resolveDowntime(teeId1); + + TEEReputation.ReputationScore memory score = reputation.calculateReputationScore(teeId1); + // Should be lower due to downtime, but still reasonable + assertLt(score.score, 10000); // Not perfect + assertGe(score.score, 0); // But not terrible + } + + function test_ReputationWithFailures() public { + // 5 successful, 5 failed + for (uint256 i = 0; i < 5; i++) { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + + vm.prank(recorder); + reputation.recordSettlement(teeId1, false, 1000); + } + + TEEReputation.ReputationScore memory score = reputation.calculateReputationScore(teeId1); + // 50% success rate + assertEq(score.tier, reputation.TIER_FAIR()); + assertGe(score.score, reputation.tierFairThreshold()); + assertLt(score.score, reputation.tierGoodThreshold()); + } + + // ============ Configuration Tests ============ + + function test_UpdateWeights() public { + vm.prank(admin); + reputation.updateWeights(6000, 3000, 1000); // 60% success, 30% uptime, 10% response + + assertEq(reputation.successWeightBps(), 6000); + assertEq(reputation.uptimeWeightBps(), 3000); + assertEq(reputation.responseWeightBps(), 1000); + } + + function test_UpdateWeightsRevertsIfNotSum10000() public { + vm.prank(admin); + vm.expectRevert("Weights must sum to 10000"); + reputation.updateWeights(5000, 3000, 1000); // Sum = 9000 + } + + function test_UpdateTierThresholds() public { + vm.prank(admin); + reputation.updateTierThresholds(8500, 6500, 3500); + + assertEq(reputation.tierExcellentThreshold(), 8500); + assertEq(reputation.tierGoodThreshold(), 6500); + assertEq(reputation.tierFairThreshold(), 3500); + } + + function test_RecalculateReputation() public { + // Record some data + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 1000); + + // Get initial cached score + TEEReputation.ReputationScore memory score1 = reputation.cachedReputation(teeId1); + + // Update weights + vm.prank(admin); + reputation.updateWeights(6000, 2000, 2000); + + // Cached score should be stale + // Recalculate + vm.prank(admin); + reputation.recalculateReputation(teeId1); + + // Get new cached score + TEEReputation.ReputationScore memory score2 = reputation.cachedReputation(teeId1); + // Scores might differ due to weight change + assertNotEq(score1.calculatedAt, score2.calculatedAt); + } + + // ============ Access Control Tests ============ + + function test_OnlyRecorderCanRecordSettlement() public { + address unauthorized = address(0x99); + + vm.prank(unauthorized); + vm.expectRevert(); + reputation.recordSettlement(teeId1, true, 1000); + } + + function test_OnlyMonitorCanRecordDowntime() public { + address unauthorized = address(0x99); + + vm.prank(unauthorized); + vm.expectRevert(); + reputation.recordDowntime(teeId1, 100, "test"); + } + + function test_OnlyAdminCanUpdateWeights() public { + address unauthorized = address(0x99); + + vm.prank(unauthorized); + vm.expectRevert(); + reputation.updateWeights(5000, 3500, 1500); + } + + // ============ Integration Tests ============ + + function test_EndToEndReputationFlow() public { + // TEE1: Good performer + for (uint256 i = 0; i < 20; i++) { + vm.prank(recorder); + reputation.recordSettlement(teeId1, true, 800); + } + + // TEE2: Poor performer + for (uint256 i = 0; i < 10; i++) { + vm.prank(recorder); + reputation.recordSettlement(teeId2, true, 800); + + vm.prank(recorder); + reputation.recordSettlement(teeId2, false, 2000); + } + + // TEE2 has downtime + vm.prank(monitor); + reputation.recordDowntime(teeId2, 0, "heartbeat_missed"); + + vm.warp(block.timestamp + 500); + + vm.prank(monitor); + reputation.resolveDowntime(teeId2); + + // Check scores + TEEReputation.ReputationScore memory score1 = + reputation.calculateReputationScore(teeId1); + TEEReputation.ReputationScore memory score2 = + reputation.calculateReputationScore(teeId2); + + assertGt(score1.score, score2.score); + assertEq(score1.tier, reputation.TIER_EXCELLENT()); + assertEq(score2.tier, reputation.TIER_FAIR()); + } +}