aere-contracts/contracts/erc8004/AereIdentityRegistry8004.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

212 lines
10 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/utils/Strings.sol";
import "./IERC8004.sol";
/// @dev The subset of the LIVE, deployed AereAgentDID (chain 2800, 0xce64…22C5) this adapter reads.
/// The DID is the Aere-native agent identity: an agent whose ROOT authority is a Falcon key in
/// AerePQCKeyRegistry. This adapter never writes to the DID and never redeploys it.
interface IAereAgentDIDIdentity {
function agentExists(uint256 rootKeyId) external view returns (bool);
function getAgent(uint256 rootKeyId)
external
view
returns (address controller, uint64 createdBlock, uint64 sessionNonce, uint256 sessionCountForAgent);
function keyRegistry() external view returns (address);
}
/// @dev The one AerePQCKeyRegistry read used to resolve an agent's CURRENT controller.
interface IAerePQCKeyRegistryOwner {
function ownerOf(uint256 keyId) external view returns (address);
}
/**
* @title AereIdentityRegistry8004, an ERC-8004 Identity adapter over the deployed AereAgentDID
*
* @notice Exposes the ERC-8004 Identity registry surface (register / resolve an agent with a DID
* plus an AgentCard metadata URI) over Aere Network's ALREADY-DEPLOYED agent identity
* primitive, AereAgentDID. It does NOT redeploy or modify the live DID: it reads the live
* DID for agent existence and control, and stores only the ERC-8004 metadata (domain +
* AgentCard URI) that the DID itself does not carry.
*
* IDENTITY ANCHOR. The ERC-8004 agentId IS the AereAgentDID agent id, which is the Falcon
* ROOT key id in AerePQCKeyRegistry (agentId == rootKeyId). So an ERC-8004 resolve is a
* direct, honest pass-through to the live DID: the agent's on-chain root of authority is a
* quantum-durable Falcon key, not a bare secp256k1 address. The DID string is
* "did:aere:<chainId>:<agentId>".
*
* REGISTER = BIND METADATA TO AN EXISTING DID AGENT. `register` does not mint a new
* identity (the identity already exists in the live DID). It binds an ERC-8004 AgentCard
* URI + domain to an existing DID agent, and is gated to that agent's CURRENT controller
* (the registry owner of the Falcon root key), so only the party who proved Falcon
* possession can publish the agent's ERC-8004 metadata.
*
* @dev [VERIFY: confirm against the finalized ERC-8004 interface] The newer ERC-8004 Identity
* draft leans on an ERC-721 registration (register(tokenURI) minting a sequential id). Aere
* anchors the ERC-8004 identity to the pre-existing Falcon-rooted DID id instead of minting
* a fresh sequential id, because the trust root is the on-chain Falcon key, not a token.
* The resolve / getAgent / AgentCard-URI SEMANTICS match ERC-8004; the id-minting selector
* differs by design and is documented here.
*
* HONEST SCOPE. Application-layer identity only. This does NOT change AERE consensus (blocks
* are still Besu QBFT with classical secp256k1 ECDSA). It holds no funds and has no admin.
*/
contract AereIdentityRegistry8004 is IERC8004Identity {
using Strings for uint256;
/// @notice The LIVE, deployed AereAgentDID this adapter presents an ERC-8004 face over.
IAereAgentDIDIdentity public immutable DID;
/// @notice The AerePQCKeyRegistry the DID roots into (read from the DID at construction). Used
/// only to resolve an agent's current controller (the registry owner of the root key).
IAerePQCKeyRegistryOwner public immutable KEY_REGISTRY;
struct Record {
bool registered;
string agentDomain;
string agentCardURI;
address boundController; // controller captured at registration (immutable per Falcon root key)
}
mapping(uint256 => Record) private _records; // agentId (== rootKeyId) => ERC-8004 metadata
mapping(bytes32 => uint256) private _domainToAgentPlus1; // keccak(domain) => agentId + 1 (0 = unset)
mapping(address => uint256[]) private _agentsOfAddress; // controller => agentIds it registered
uint256[] private _registeredAgentIds; // append-only list of all registered agentIds
error ZeroAddress();
error UnknownAgent(uint256 agentId);
error NotController(uint256 agentId);
error AlreadyRegistered(uint256 agentId);
error NotRegistered(uint256 agentId);
error EmptyDomain();
error DomainTaken(string agentDomain);
constructor(address did) {
if (did == address(0)) revert ZeroAddress();
DID = IAereAgentDIDIdentity(did);
address reg = IAereAgentDIDIdentity(did).keyRegistry();
if (reg == address(0)) revert ZeroAddress();
KEY_REGISTRY = IAerePQCKeyRegistryOwner(reg);
}
// ==========================================================================
// Registration (metadata binding)
// ==========================================================================
/**
* @notice Bind ERC-8004 metadata (domain + AgentCard URI) to an existing AereAgentDID agent.
* Gated to the agent's CURRENT controller (the registry owner of the Falcon root key).
* Does not mint a new identity: the identity already lives in the live DID.
* @param agentId the AereAgentDID agent id (== the Falcon root key id).
* @param agentDomain the agent's domain (non-empty, unique across this adapter).
* @param agentCardURI the AgentCard / registration metadata document URI.
*/
function register(uint256 agentId, string calldata agentDomain, string calldata agentCardURI) external {
if (!DID.agentExists(agentId)) revert UnknownAgent(agentId);
address controller = KEY_REGISTRY.ownerOf(agentId);
if (controller == address(0) || msg.sender != controller) revert NotController(agentId);
if (_records[agentId].registered) revert AlreadyRegistered(agentId);
if (bytes(agentDomain).length == 0) revert EmptyDomain();
bytes32 dk = keccak256(bytes(agentDomain));
if (_domainToAgentPlus1[dk] != 0) revert DomainTaken(agentDomain);
_records[agentId] =
Record({registered: true, agentDomain: agentDomain, agentCardURI: agentCardURI, boundController: controller});
_domainToAgentPlus1[dk] = agentId + 1;
_agentsOfAddress[controller].push(agentId);
_registeredAgentIds.push(agentId);
emit AgentRegistered(agentId, agentDomain, agentCardURI, controller);
}
/**
* @notice Update the ERC-8004 metadata for an already-registered agent. Controller-gated.
* Re-indexes the domain (the new domain must be free unless unchanged).
*/
function updateAgent(uint256 agentId, string calldata newDomain, string calldata newCardURI) external {
Record storage r = _records[agentId];
if (!r.registered) revert NotRegistered(agentId);
address controller = KEY_REGISTRY.ownerOf(agentId);
if (controller == address(0) || msg.sender != controller) revert NotController(agentId);
if (bytes(newDomain).length == 0) revert EmptyDomain();
bytes32 oldKey = keccak256(bytes(r.agentDomain));
bytes32 newKey = keccak256(bytes(newDomain));
if (newKey != oldKey) {
if (_domainToAgentPlus1[newKey] != 0) revert DomainTaken(newDomain);
delete _domainToAgentPlus1[oldKey];
_domainToAgentPlus1[newKey] = agentId + 1;
r.agentDomain = newDomain;
}
r.agentCardURI = newCardURI;
emit AgentUpdated(agentId, newDomain, newCardURI, controller);
}
// ==========================================================================
// Resolve
// ==========================================================================
/// @inheritdoc IERC8004Identity
function getAgent(uint256 agentId) public view returns (AgentInfo memory) {
Record storage r = _records[agentId];
if (!r.registered) revert NotRegistered(agentId);
return AgentInfo({
agentId: agentId,
did: didString(agentId),
agentDomain: r.agentDomain,
agentCardURI: r.agentCardURI,
agentAddress: KEY_REGISTRY.ownerOf(agentId), // current controller, read live
registered: true
});
}
/// @inheritdoc IERC8004Identity
function resolveByDomain(string calldata agentDomain) external view returns (AgentInfo memory) {
uint256 plus1 = _domainToAgentPlus1[keccak256(bytes(agentDomain))];
if (plus1 == 0) revert NotRegistered(0);
return getAgent(plus1 - 1);
}
/// @inheritdoc IERC8004Identity
/// @dev Returns the MOST RECENT agent registered by `agentAddress`. Use agentsOf() for the full set.
function resolveByAddress(address agentAddress) external view returns (AgentInfo memory) {
uint256[] storage ids = _agentsOfAddress[agentAddress];
if (ids.length == 0) revert NotRegistered(0);
return getAgent(ids[ids.length - 1]);
}
/// @inheritdoc IERC8004Identity
function agentCount() external view returns (uint256) {
return _registeredAgentIds.length;
}
// ==========================================================================
// Views
// ==========================================================================
/// @notice True iff ERC-8004 metadata has been bound for `agentId` (and the DID agent exists).
function isRegistered(uint256 agentId) external view returns (bool) {
return _records[agentId].registered && DID.agentExists(agentId);
}
/// @notice All agentIds registered by a controller (append-only; ids are stable Falcon roots).
function agentsOf(address controller) external view returns (uint256[] memory) {
return _agentsOfAddress[controller];
}
/// @notice The full list of registered agentIds (append-only).
function registeredAgentIds() external view returns (uint256[] memory) {
return _registeredAgentIds;
}
/// @notice The DID string for an agent: "did:aere:<chainId>:<agentId>".
function didString(uint256 agentId) public view returns (string memory) {
return string.concat("did:aere:", block.chainid.toString(), ":", agentId.toString());
}
}