Recentemente comecei os estudos da ERC-8004: Trustless Agents e resolvi escrever aqui no site para compartilhar o que aprendi sobre o assunto. Na primeira parte desta série de tutoriais, falamos da problemática A2A que gerou a motivação para a criação da ERC-8004 e entramos a fundo no primeiro dos seus três pilares, que é o Registro de Identidade. Nesta segunda etapa, vamos aprofundar no segundo pilar: Registro de Reputação.
Vamos lá!
Registro de Reputação
Uma vez que o smart contract Registro de Identidade garante que o Agente de IA é quem diz ser, entrega os serviços que ele realiza, quem é o seu dono e muito mais, é com o Registro de Reputação que sabemos se esse agente é realmente bom no que ele faz. Pense no registro de reputação como sendo aquela seção de avaliações que temos em qualquer site de ecommerce ou mesmo de prestação de serviços, onde geralmente temos estrelinhas ou notas de 1-5, 1-10, etc, além de depoimentos de clientes satisfeitos (ou não). A ideia aqui é a mesma, mas fazer isso mantendo um registro on-chain (auditável, imutável e descentralizado) e eventualmente, caso necessário, mais alguma informação complementar off-chain.
Assim, toda vez que um Agente, devidamente registrado na blockchain, realiza um serviço para outro agente, ele autoriza o mesmo a deixar uma avaliação no registro de reputação, que nada mais é que outro smart contract fracamento acoplado ao registro de identidade, através de uma função initialize.
|
1 2 3 |
function initialize(address identityRegistry_) |
Mais tarde, para saber quem é o identityRegistry, deve-se usar outra função.
|
1 2 3 |
function getIdentityRegistry() external view returns (address identityRegistry) |
Feedback
Essa avaliação pode ser qualquer coisa a depender do contexto: notas de 1-10, estrelas de 1-5, etc. O formato sugerido pelo draft da ERC-8004 sugere que os feedback sejam no seguinte formato:
- valor do score (inteiro);
- número de casas decimais do score (igual temos em tokens ERC-20);
- até duas tags opcionais, para ajudar a entender o score;
- endpoint de contexto (do serviço usado, opcional);
- arquivo JSON off-chain com detalhes (IPFS, opcional);
- hash do arquivo anterior (opcional);
Como pode ver, a única coisa realmente importante aqui é o score, um int128 com a nota que o agente dá pelo serviço do outro agente e o número de casas decimais que vamos usar (podendo ser 0). Além de armazenar os scores dos feedbacks individuais, a ERC determina que a agregação dos mesmos, de maneira simplificada, seja feita on-chain, ou seja, esteja presente no contrato, enquanto que agregações e relatórios mais sofisticados possam ser feitos off-chain, se necessário. É nesses recursos off-chain onde os campos opcionais do feedback podem brilhar, já que para uma média simples apenas o score e decimais são o suficiente.
Para dar feeback por um serviço, a função a ser chamada é essa:
|
1 2 3 |
function giveFeedback(uint256 agentId, int128 value, uint8 valueDecimals, string calldata tag1, string calldata tag2, string calldata endpoint, string calldata feedbackURI, bytes32 feedbackHash) external |
Importante ressaltar que o owner de um agent não pode deixar feedback sobre ele, bem como carteiras autorizadas a transferi-lo. Caso esteja fazendo os agents como soulbound tokens, essa última instrução não se aplica, já que a transferência não é possível.
Quando um feedback é realizado, um evento deve ser emitido, como abaixo.
|
1 2 3 |
event NewFeedback(uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, int128 value, uint8 valueDecimals, string indexed indexedTag1, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash) |
A ERC ainda sugere tags para contextualizar o score (como starred, uptime e outras) e padrão de metadados para o arquivo off-chain da avaliação.
Um ponto que não é citado é sobre autorização para feedback, já que um sistema aberto poderia facilmente seria inundado por spam para denegrir ou elevar a reputação de um agente virtualmente. Isso pode ser resolvido criando um mapping semelhante ao de allowance do ERC-20, para que o owner do agente autorize automaticamente o recebimento do feedback após a prestação do serviço.
Lidando com Feedbacks
A ERC também determina corner cases envolvendo feedbacks, como por exemplo no caso de um client querendo revogar um feedback que deu a um agente. Neste caso, ele precisa chamar a função revokeFeedback:
|
1 2 3 |
function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external |
E ela por sua vez deve emitir um evento FeedbackRevoked:
|
1 2 3 |
event FeedbackRevoked(uint256 indexed agentId, address indexed clientAddress, uint64 indexed feedbackIndex) |
Também prevê a adição de respostas a feedbacks, por exemplo o agent owner apresentando recibo de refund ou o client apresentando recibo de transação. Para adicionar respostas, deve ser chamada a função appendResponse:
|
1 2 3 |
function appendResponse(uint256 agentId, address clientAddress, uint64 feedbackIndex, string calldata responseURI, bytes32 responseHash) external |
Essa função usa o índice do feedback como associação e permite fornecer a URI do arquivo na rede IPFS. No caso de uso de outras redes, o responseHash é obrigatório para garantir integridade.
Por fim, temos as funções de leitura definidas pelo padrão:
|
1 2 3 4 5 6 7 8 |
function getSummary(uint256 agentId, address[] calldata clientAddresses, string tag1, string tag2) external view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals) function readFeedback(uint256 agentId, address clientAddress, uint64 feedbackIndex) external view returns (int128 value, uint8 valueDecimals, string tag1, string tag2, bool isRevoked) function readAllFeedback(uint256 agentId, address[] calldata clientAddresses, string tag1, string tag2, bool includeRevoked) external view returns (address[] memory clients, uint64[] memory feedbackIndexes, int128[] memory values, uint8[] memory valueDecimals, string[] memory tag1s, string[] memory tag2s, bool[] memory revokedStatuses) function getResponseCount(uint256 agentId, address clientAddress, uint64 feedbackIndex, address[] responders) external view returns (uint64 count) function getClients(uint256 agentId) external view returns (address[] memory) function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint64) |
A getSummary deve retornar um resumo agregado dos feedbacks dados para um agentId por um conjunto de clients. Ou seja, é uma subvisão da agregação geral daquele agente.
A readFeedback deve trazer um feedback específico, considerando o client e/ou o índice do feedback. Opcionalmente pode-se usar as tags para filtrar ainda mais o retorno.
A readAllFeedback recebe o agentId obrigatório e alguns filtros opcionais e retorna a coleção de clients, índices de feedback, notas dadas, decimais, tags e a informação se cada um está revogado ou não. Note que não são retornados os feedbacks inteiros em si e caso sejam passados filtros, estes devem ser repeitados no resultado final.
A getResponseCount traz o número de respostas para um feedback e agente específicos, podendo ainda filtras por client e/ou responder.
A getClients traz todos os endereços de clientes que deixaram feedback para este agente.
E por fim, a getLastIndex deve retornar o índice do último feedbakc submetido por um client para um agentId.
Abaixo, um exemplo mínimo de implementação de ReputationRegistry, com acoplamento fraco no IdentityRegistry.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; interface IERC8004IdentityRegistry { function exists(uint256 agentId) external view returns (bool); function ownerOf(uint256 agentId) external view returns (address); } contract ReputationRegistry { event NewFeedback( uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, int128 value, uint8 valueDecimals ); event FeedbackRevoked(uint256 indexed agentId, address indexed clientAddress, uint64 indexed feedbackIndex); event ResponseAppended(uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, address indexed responder, string responseURI, bytes32 responseHash); struct Feedback { uint256 agentId; address from; int128 value; uint8 valueDecimals; uint256 timestamp; bool revoked; } struct Response { address responder; string responseURI; bytes32 responseHash; uint256 timestamp; } struct Aggregate { int256 sum; uint256 count; uint256 avg; } uint8 constant DECIMALS = 2; IERC8004IdentityRegistry private _identityRegistry; mapping(uint256 => Feedback[]) private _feedbacks; mapping(uint256 => mapping(uint256 => Response[])) public responses; mapping(uint256 => Aggregate) public aggregate; mapping(uint256 => mapping(address => bool)) private _feedbackAuthorizations; function initialize(address identityRegistry) external { require(address(_identityRegistry) == address(0), "Already initialized"); require(identityRegistry != address(0), "Invalid address"); _identityRegistry = IERC8004IdentityRegistry(identityRegistry); } function getIdentityRegistry() external view returns (address) { return address(_identityRegistry); } function authorizeFeedback(uint256 agentId, address clientAddress) external { require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); require(_identityRegistry.ownerOf(agentId) == msg.sender, "Only agent owner can authorize"); _feedbackAuthorizations[agentId][clientAddress] = true; } function giveFeedback( uint256 agentId, int128 value, uint8 valueDecimals ) external { require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); require(valueDecimals == DECIMALS, "Invalid decimals"); require(_feedbackAuthorizations[agentId][msg.sender] && _identityRegistry.ownerOf(agentId) != msg.sender, "Not authorized to give feedback"); _feedbackAuthorizations[agentId][msg.sender] = false; // Revoke authorization to prevent reentrancy _feedbacks[agentId].push( Feedback({ agentId: agentId, from: msg.sender, value: value, valueDecimals: DECIMALS, timestamp: block.timestamp, revoked: false }) ); Aggregate storage agg = aggregate[agentId]; agg.sum += value; agg.count += 1; agg.avg = agg.sum / agg.count; emit NewFeedback( agentId, msg.sender, _feedbacks[agentId].length - 1, value, DECIMALS ); } function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external { require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); require(feedbackIndex < _feedbacks[agentId].length, "Invalid index"); Feedback memory feedback = _feedbacks[agentId][feedbackIndex]; require(feedback.from == msg.sender, "Not feedback author"); // Update aggregate before removing feedback Aggregate storage agg = aggregate[agentId]; agg.sum -= feedback.value; agg.count -= 1; agg.avg = agg.count > 0 ? agg.sum / agg.count : int256(0); _feedbacks[agentId][feedbackIndex].revoked = true; // Mark as revoked instead of removing to maintain indices emit FeedbackRevoked(agentId, msg.sender, feedbackIndex); } function appendResponse(uint256 agentId, address clientAddress, uint64 feedbackIndex, string calldata responseURI, bytes32 responseHash) external { require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); require(feedbackIndex < _feedbacks[agentId].length, "Invalid index"); bool isAgentOwner = clientAddress == msg.sender && _identityRegistry.ownerOf(agentId) == msg.sender; bool isClient = clientAddress == msg.sender && clientAddress == _feedbacks[agentId][feedbackIndex].from; require(isAgentOwner || isClient, "Only agent owner or feedback author can respond"); responses[agentId][feedbackIndex].push( Response({ responder: msg.sender, responseURI: responseURI, responseHash: responseHash, timestamp: block.timestamp }) ); emit ResponseAppended( agentId, clientAddress, feedbackIndex, msg.sender, responseURI, responseHash ); } function contains(address[] memory addrSet, address addr) private pure returns (bool){ for(uint256 i=0; i < addrSet.length; i++){ if(addr == addrSet[i]) return true; } return false; } function getSummary(uint256 agentId, address[] calldata clientAddresses, string tag1, string tag2) external view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals){ require(clientAddresses.length > 0, "clientAddresses is required"); require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); count = 0; summaryValue = 0; summaryValueDecimals = DECIMALS; for (uint256 i = 0; i < _feedbacks[agentId].length; i++) { Feedback memory feedback = _feedbacks[agentId][i]; if (!feedback.revoked && contains(clientAddresses, feedback.from)) { summaryValue += feedback.value; count++; } } summaryValue = count > 0 ? summaryValue / count : int128(0); } function readFeedback(uint256 agentId, address clientAddress, uint64 feedbackIndex) external view returns (int128 value, uint8 valueDecimals, string tag1, string tag2, bool isRevoked) { require(feedbackIndex < _feedbacks[agentId].length, "Invalid index"); Feedback memory feedback; if(clientAddress != address(0)) { uint64 aux = 0; for(uint64 i = 0; i < _feedbacks[agentId].length; i++) { if(_feedbacks[agentId][i].from == clientAddress){ if(aux == feedbackIndex) { feedback = _feedbacks[agentId][i]; break; } else aux++; } } require(feedback.from == clientAddress, "Feedback not found for specified client"); } else { feedback = _feedbacks[agentId][feedbackIndex]; } value = feedback.value; valueDecimals = feedback.valueDecimals; tag1 = ""; // Placeholder, as tags are not implemented in this example tag2 = ""; // Placeholder, as tags are not implemented in this example isRevoked = feedback.revoked; } function readAllFeedback(uint256 agentId, address[] calldata clientAddresses, string tag1, string tag2, bool includeRevoked) external view returns (address[] memory clients, uint64[] memory feedbackIndexes, int128[] memory values, uint8[] memory valueDecimals, string[] memory tag1s, string[] memory tag2s, bool[] memory revokedStatuses){ require(clientAddresses.length > 0, "clientAddresses is required"); require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); // For simplicity, this example does not implement tag-based filtering. In a real implementation, you would need to store tags in the Feedback struct and filter accordingly. uint64 count = 0; for (uint64 i = 0; i < _feedbacks[agentId].length; i++) { Feedback memory feedback = _feedbacks[agentId][i]; if ((includeRevoked || !feedback.revoked) && (clientAddresses.length == 0 || contains(clientAddresses, feedback.from))) { count++; } } clients = new address[](count); feedbackIndexes = new uint64[](count); values = new int128[](count); valueDecimals = new uint8[](count); tag1s = new string[](count); tag2s = new string[](count); revokedStatuses = new bool[](count); uint64 index = 0; for (uint64 i = 0; i < _feedbacks[agentId].length; i++) { Feedback memory feedback = _feedbacks[agentId][i]; if ((includeRevoked || !feedback.revoked) && (clientAddresses.length == 0 || contains(clientAddresses, feedback.from))) { clients[index] = feedback.from; feedbackIndexes[index] = i; values[index] = feedback.value; valueDecimals[index] = feedback.valueDecimals; tag1s[index] = ""; // Placeholder, as tags are not implemented in this example tag2s[index] = ""; // Placeholder, as tags are not implemented in this example revokedStatuses[index] = feedback.revoked; index++; } } } function getResponseCount(uint256 agentId, address clientAddress, uint64 feedbackIndex, address[] responders) external view returns (uint64 count){ require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); count = 0; bool clientFilter = clientAddress != address(0); bool responderFilter = responders.length > 0; if(clientFilter){ uint64 aux = 0; for (uint64 i = 0; i < _feedbacks[agentId].length; i++) { if (_feedbacks[agentId][i].from == clientAddress) { if(aux == feedbackIndex) { for (uint64 j = 0; j < responses[agentId][i].length; j++) { Response memory response = responses[agentId][i][j]; if (!responderFilter || contains(responders, response.responder)) { count++; } } } else aux++; } } } else { for (uint64 i = 0; i < responses[agentId][feedbackIndex].length; i++) { Response memory response = responses[agentId][feedbackIndex][i]; if (!responderFilter || contains(responders, response.responder)) { count++; } } } } function getClients(uint256 agentId) external view returns (address[] memory){ require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); uint256 count = _feedbacks[agentId].length; address[] memory clients = new address[](count); for(uint256 i=0; i < count; i++){ clients[i] = _feedbacks[agentId][i].from; } return clients; } function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint256){ require(address(_identityRegistry) != address(0), "Not initialized"); require(_identityRegistry.exists(agentId), "Unknown agent"); require(clientAddress != address(0), "Invalid client address"); for (uint256 i = _feedbacks[agentId].length - 1; i >= 0; i--) { if (_feedbacks[agentId][i].from == clientAddress && !_feedbacks[agentId][i].revoked) { return i; } } revert("No feedback from this client"); } } |
Na próxima e última etapa, vamos falar do terceiro pilar, o Registro de Validação.
Até lá!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.



