Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions rust/contracts/src/bindings/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ mod registry_inner {
function unpublish(bytes32 platformId) external;
function resolveId(bytes32 platformId, string calldata id) external view returns (address);
function resolveHandle(bytes32 platformId, string calldata handle) external view returns (address);
/// The holder that proved an id and when, as `resolveId` reads
/// it; `(0, 0)` when nobody did.
function idBindingOf(bytes32 platformId, string calldata id) external view returns (address holder, uint64 observedAt);
/// The holder that last proved a handle and when, as
/// `resolveHandle` reads it. `(0, 0)` for text the rules refuse;
/// a retired handle answers a zero holder beside its watermark.
function handleBindingOf(bytes32 platformId, string calldata handle) external view returns (address holder, uint64 observedAt);
/// The handle an id proved most recently, and whether the handle
/// node still points back at that identity. Empty and false for an
/// id never proved. `current` reads the nodes, not the rules, so it
/// stays true after the rules narrow and the handle no longer
/// resolves. Reverts `UnknownPlatform` for an unwired platform.
function handleOfId(bytes32 platformId, string calldata id) external view returns (string memory handle, bool current);
/// The id of the identity that holds a handle now. Empty when
/// nobody holds it, or for text the rules refuse. Reverts
/// `UnknownPlatform` for an unwired platform.
function idOfHandle(bytes32 platformId, string calldata handle) external view returns (string memory id);
/// The handle's holder, and whether `id` resolves to that same
/// holder.
function resolveHandleAndId(bytes32 platformId, string calldata handle, string calldata id) external view returns (address holder, bool idAgrees);
Expand All @@ -88,6 +105,9 @@ mod registry_inner {
/// `keccak256` of the normalized handle: what `HandleEscrow.deposit`
/// takes. Reverts `UnknownPlatform` or `UnusableHandle`.
function handleHashOf(bytes32 platformId, string calldata handle) external view returns (bytes32 handleHash);
/// The handle normalized under the platform's current rules.
/// Reverts `UnknownPlatform` or `UnusableHandle`.
function normalizeHandle(bytes32 platformId, string calldata handle) external view returns (string memory normalized);
/// The node a handle hashes to now; reverts as `handleHashOf` does.
function handleNodeOf(bytes32 platformId, string calldata handle) external view returns (bytes32 handleNode);
/// The node of a handle given as its hash, as `bind` binds it.
Expand Down
18 changes: 12 additions & 6 deletions solidity/contracts/ens/HandleResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,13 @@ import {IExtendedResolver} from "./IExtendedResolver.sol";
/// transactions, with no gateway key involved. This contract does not
/// split that and cannot; what limits the damage is where the owner key
/// lives, and that every change is a transaction visible in the registry.
/// Holding the owner key, the deployer key and the ENS name's owner as ONE
/// identity — which the current setup does — means one compromise reaches
/// all three. See `libid/design/ens-integration.md`.
/// libID's ENS integration spec (`specs/ens-integration.md`, REQ-ENS-KEY-01)
/// keeps three keys apart: the DNS registrar account that holds the
/// Parent Name, the key that owns the Parent Name in ENS and this
/// resolver, and each Signer. Held together, the least guarded one
/// decides all three. The spec records that the current deployment does
/// not meet it: one key, the one the deploy workflow signs with, deploys
/// this resolver, owns it, and owns the Parent Name.
///
/// The signature covers the resolver, an expiry, the request and the
/// result, so an answer cannot be replayed against another resolver,
Expand All @@ -53,9 +57,11 @@ import {IExtendedResolver} from "./IExtendedResolver.sol";
/// `ensdomains/offchain-resolver`'s `SignatureVerifier`, which the test
/// suite pins byte for byte. So a deployment discipline stands in for it:
/// an answer signed for a resolver at address A on one chain verifies
/// unchanged at address A on another. Never share a signing key between
/// deployments that could land on the same address — which deterministic
/// deployment makes the usual outcome rather than the exotic one.
/// unchanged at address A on another. Resolvers that trust one Signer
/// must sit at different addresses, whichever chain they are on
/// (REQ-ENS-KEY-02): one Signer may serve several chains only while the
/// addresses differ, and deterministic deployment makes a shared address
/// the usual outcome rather than the exotic one.
///
/// **Not upgradeable, on purpose.** Replacing it is `setResolver` on the
/// ENS name, which is one owner transaction and visible in the registry.
Expand Down
24 changes: 24 additions & 0 deletions solidity/contracts/escrow/test/EscrowMocks.sol
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ contract SettableRegistry is IIdentityRegistry {
function acceptsBindings(bytes32 platformId) external pure returns (bool) {
return platformId != bytes32(0);
}

// The registry's one-call reads are not modelled: escrow reads holders
// through `handleBinding` alone. Each reverts, so a test that starts
// depending on one fails here instead of reading a made-up answer.

function normalizeHandle(bytes32, string calldata) external pure returns (string memory) {
revert("SettableRegistry: normalizeHandle not modelled");
}

function handleBindingOf(bytes32, string calldata) external pure returns (address, uint64) {
revert("SettableRegistry: handleBindingOf not modelled");
}

function idBindingOf(bytes32, string calldata) external pure returns (address, uint64) {
revert("SettableRegistry: idBindingOf not modelled");
}

function handleOfId(bytes32, string calldata) external pure returns (string memory, bool) {
revert("SettableRegistry: handleOfId not modelled");
}

function idOfHandle(bytes32, string calldata) external pure returns (string memory) {
revert("SettableRegistry: idOfHandle not modelled");
}
}

/// @notice A plain ERC-20 anybody can mint.
Expand Down
41 changes: 39 additions & 2 deletions solidity/contracts/identity/IIdentityRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ pragma solidity ^0.8.20;

import {HandleNormalizer} from "./HandleNormalizer.sol";

/// @notice What other contracts, `HandleEscrow` among them, ask the identity
/// registry.
/// @notice The identity registry's reads: what `HandleEscrow` and other
/// contracts ask, and the one-call reads a client resolves with.
interface IIdentityRegistry {
/// This platform is not configured.
error UnknownPlatform(bytes32 platformId);
Expand All @@ -20,6 +20,13 @@ interface IIdentityRegistry {
/// rules. Reverts `UnknownPlatform` or `UnusableHandle`.
function handleHashOf(bytes32 platformId, string calldata handle) external view returns (bytes32 handleHash);

/// @notice A handle normalized under the platform's current rules. Reverts
/// as `handleHashOf` does.
function normalizeHandle(bytes32 platformId, string calldata handle)
external
view
returns (string memory normalized);

/// @notice The node a handle hashes to under the platform's current rules.
/// Reverts as `handleHashOf` does.
function handleNodeOf(bytes32 platformId, string calldata handle) external view returns (bytes32 handleNode);
Expand All @@ -30,4 +37,34 @@ interface IIdentityRegistry {

/// @notice Whether `bind` can bind a holder on this platform now.
function acceptsBindings(bytes32 platformId) external view returns (bool);

/// @notice The holder of this handle, normalized on chain, and when it was
/// last proved. `(0, 0)` for text the rules refuse; a retired
/// handle keeps its `observedAt`. Reverts `UnknownPlatform` for a
/// platform that is not wired.
function handleBindingOf(bytes32 platformId, string calldata handle)
external
view
returns (address holder, uint64 observedAt);

/// @notice The holder that proved this id, and when. Reverts
/// `UnknownPlatform` for a platform that is not wired.
function idBindingOf(bytes32 platformId, string calldata id)
external
view
returns (address holder, uint64 observedAt);

/// @notice The handle this id proved most recently, and whether the handle
/// node still points back at this identity. `("", false)` for an id
/// never proved. Reverts `UnknownPlatform` for a platform that is
/// not wired.
function handleOfId(bytes32 platformId, string calldata id)
external
view
returns (string memory handle, bool current);

/// @notice The id of the identity that holds this handle now. Empty when
/// nobody holds it, or for text the rules refuse. Reverts
/// `UnknownPlatform` for a platform that is not wired.
function idOfHandle(bytes32 platformId, string calldata handle) external view returns (string memory id);
}
140 changes: 116 additions & 24 deletions solidity/contracts/identity/IdentityRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -817,26 +817,37 @@ contract IdentityRegistry is

// ─── Reading ────────────────────────────────────────────────────

// `rulesOf`, `handleHashOf` and `handleNodeOf` answer from the rules, so
// they need a configured platform and nothing more: they agree with each
// other, and keep answering before a platform's first verifier and after
// its last.
// `rulesOf`, `normalizeHandle`, `handleHashOf` and `handleNodeOf` answer
// from the rules, so they need a configured platform and nothing more:
// they agree with each other, and keep answering before a platform's first
// verifier and after its last.

/// @notice The platform's normalization rules as configured now, for a
/// client that normalizes locally. Reverts `UnknownPlatform`.
function rulesOf(bytes32 platformId) external view returns (HandleNormalizer.Rules memory) {
return _requireConfigured(platformId).rules;
}

/// @notice A handle normalized under the platform's current rules: the
/// string a proof of it binds, and the one `handleHashOf` hashes.
/// @dev Reverts `UnknownPlatform` for a platform with no rules, and
/// `UnusableHandle` for text the rules refuse (where `resolveHandle`
/// answers zero).
function normalizeHandle(bytes32 platformId, string calldata handle)
public
view
returns (string memory normalized)
{
HandleNormalizer.Problem problem;
(problem, normalized) = HandleNormalizer.tryNormalize(handle, _requireConfigured(platformId).rules);
if (problem != HandleNormalizer.Problem.None) revert UnusableHandle(problem);
}

/// @notice `keccak256` of a handle normalized under the platform's current
/// rules: the `handleHash` `HandleEscrow.deposit` takes.
/// @dev Reverts `UnusableHandle` for text the rules refuse (where
/// `resolveHandle` answers zero).
/// @dev Reverts as `normalizeHandle` does.
function handleHashOf(bytes32 platformId, string calldata handle) public view returns (bytes32 handleHash) {
(HandleNormalizer.Problem problem, string memory normalized) =
HandleNormalizer.tryNormalize(handle, _requireConfigured(platformId).rules);
if (problem != HandleNormalizer.Problem.None) revert UnusableHandle(problem);
return keccak256(bytes(normalized));
return keccak256(bytes(normalizeHandle(platformId, handle)));
}

/// @notice The node a handle hashes to under the platform's current rules,
Expand Down Expand Up @@ -867,8 +878,20 @@ contract IdentityRegistry is
/// proved this" to a question that was never asked — the platform is
/// not wired — and a caller cannot tell the two apart from a zero.
function resolveId(bytes32 platformId, string calldata id) external view returns (address) {
(address holder,) = idBindingOf(platformId, id);
return holder;
}

/// @notice The holder that proved this id and when the platform stated
/// it, or `(0, 0)`. Reverts as `resolveId` does.
function idBindingOf(bytes32 platformId, string calldata id)
public
view
returns (address holder, uint64 observedAt)
{
_requireUsable(platformId);
return _s().idBindings[IdentityNodes.idNode(platformId, id)].holder;
Binding storage b = _s().idBindings[IdentityNodes.idNode(platformId, id)];
return (b.holder, b.observedAt);
}

/// @notice The holder that last proved this handle, or the zero address.
Expand All @@ -883,9 +906,84 @@ contract IdentityRegistry is
/// `UnknownPlatform`. An unwired platform still reverts, because that
/// question was never asked.
function resolveHandle(bytes32 platformId, string calldata handle) external view returns (address) {
(address holder,) = handleBindingOf(platformId, handle);
return holder;
}

/// @notice The holder that last proved this handle and when the platform
/// stated it, read the way `resolveHandle` reads it.
///
/// @dev Text the rules refuse answers `(0, 0)`; an unwired platform
/// reverts `UnknownPlatform`. A retired handle answers a zero holder
/// beside the `observedAt` of the proof that last held it, which is
/// the watermark a new proof of it has to beat.
function handleBindingOf(bytes32 platformId, string calldata handle)
public
view
returns (address holder, uint64 observedAt)
{
(bool normalizes, bytes32 handleNode) = _usableHandleNode(platformId, handle);
if (!normalizes) return (address(0), 0);
Binding storage b = _s().handleBindings[handleNode];
return (b.holder, b.observedAt);
}

/// @notice The id of the identity that holds this handle now, or empty.
///
/// @dev Live: answers only while the handle has a holder, so it agrees
/// with `resolveHandle`. Empty when nobody proved the handle, when the
/// identity that proved it has since proved another one, and for text
/// the rules refuse. The `idNodeByHandle` pointer it reads keeps
/// naming the identity that renamed away; an indexer mirroring that
/// pointer under a similar name answers differently from this. An
/// unwired platform reverts `UnknownPlatform`. The id is byte for byte
/// as the platform issued it.
function idOfHandle(bytes32 platformId, string calldata handle) external view returns (string memory id) {
(bool normalizes, bytes32 handleNode) = _usableHandleNode(platformId, handle);
if (!normalizes || _s().handleBindings[handleNode].holder == address(0)) return "";
return _s().idPreimages[_s().idNodeByHandle[handleNode]].id;
}

/// @notice The handle this id proved most recently, and whether it is
/// still this identity's. Empty and false for an id never proved.
///
/// @dev `current` is `identitiesOf`'s `handleCurrent`: the handle node
/// points back at this identity. It turns false once another
/// identity proves the handle, and the string stays as the last
/// thing this identity was known as. It reads the nodes, not the
/// rules: after the owner narrows a platform's rules so the handle no
/// longer normalizes, `current` stays true while `resolveHandle`,
/// `handleBindingOf` and `idOfHandle` answer nobody for that text.
/// The handle comes from the `handleNodeById` pointer, which an
/// indexer may mirror under a similar name; `current` is what this
/// adds to it. Reverts as `resolveId` does.
function handleOfId(bytes32 platformId, string calldata id)
external
view
returns (string memory handle, bool current)
{
_requireUsable(platformId);
return _handleOf(IdentityNodes.idNode(platformId, id));
}

/// @dev The node a resolver reads for this handle, behind the resolvers'
/// gate. `normalizes` is false for text the rules refuse.
function _usableHandleNode(bytes32 platformId, string calldata handle)
private
view
returns (bool normalizes, bytes32 handleNode)
{
Platform memory platform = _requireUsable(platformId);
(HandleNormalizer.Problem problem, bytes32 handleNode) = _handleNode(platformId, handle, platform.rules);
return problem == HandleNormalizer.Problem.None ? _s().handleBindings[handleNode].holder : address(0);
HandleNormalizer.Problem problem;
(problem, handleNode) = _handleNode(platformId, handle, platform.rules);
normalizes = problem == HandleNormalizer.Problem.None;
}

/// @dev The handle an identity proved most recently, as normalized when it
/// was written, and whether its node still points back at the identity.
function _handleOf(bytes32 idNode) private view returns (string memory handle, bool current) {
bytes32 handleNode = _s().handleNodeById[idNode];
return (_s().handlePreimages[handleNode], _s().idNodeByHandle[handleNode] == idNode);
}

/// @dev The node a handle hashes to under the given rules, or the problem
Expand Down Expand Up @@ -955,14 +1053,10 @@ contract IdentityRegistry is
out = new Identity[](nodes.length);
for (uint256 i = 0; i < nodes.length; i++) {
bytes32 idNode = nodes[i];
bytes32 handleNode = $.handleNodeById[idNode];
IdentityPreimage storage preimage = $.idPreimages[idNode];
out[i] = Identity({
platformId: preimage.platformId,
id: preimage.id,
handle: $.handlePreimages[handleNode],
handleCurrent: $.idNodeByHandle[handleNode] == idNode
});
(string memory handle, bool current) = _handleOf(idNode);
out[i] =
Identity({platformId: preimage.platformId, id: preimage.id, handle: handle, handleCurrent: current});
}
}

Expand All @@ -985,10 +1079,8 @@ contract IdentityRegistry is
view
returns (address holder, bool idAgrees)
{
Platform memory platform = _requireUsable(platformId);

(HandleNormalizer.Problem problem, bytes32 handleNode) = _handleNode(platformId, handle, platform.rules);
holder = problem == HandleNormalizer.Problem.None ? _s().handleBindings[handleNode].holder : address(0);
(bool normalizes, bytes32 handleNode) = _usableHandleNode(platformId, handle);
if (normalizes) holder = _s().handleBindings[handleNode].holder;

address idHolder = _s().idBindings[IdentityNodes.idNode(platformId, id)].holder;
// An unknown id does not agree either. A caller with an id the chain
Expand Down
Loading
Loading