# FIP: Onchain Signers Source: https://github.com/farcasterxyz/protocol/discussions/103 Kiwi News: https://news.kiwistand.com/stories/FIP-Onchain-Signers?index=0x64aac5a2aa21af3e0dc25223a2df232c47b9b614986064fbb5cda48bcdbaddb9000eaf19 Submitted by tim on 2023-07-09 UTC, 3 upvotes, 0 comments ## Summary Farcaster's proposed FIP would track app signers onchain in a Key Registry on an L2 contract instead of in the current offchain CRDT, to avoid usability problems. The proposal maps keys to fids rather than custody addresses, so the signer relationship persists when an fid changes custody, and onchain storage avoids the CRDT's signer limit, where adding a signer past the cap removes the earliest one. Under the proposal, the Key Registry is deployed to the L2 at a stated address, and after a migration timestamp is set, hubs will reject offchain signers created at or after that timestamp. ## Article Onchain Signers Title: Onchain Signers Type: Implementation FIP Authors: horsefacts, sanjay, v Abstract Signers should be set onchain on an L2 contract to avoid usability problems with CRDT consensus. Problem Signers are used to delegate permissions to post on a user's behalf without giving up control over the Farcaster identity. The Signer is a key pair whose public key is signed by the fid's custody address and stored on a hub. Signers are tracked using a CRDT which allows keys to be added and removed. There are a few usability problems in the current implementation: The CRDT must have a limit on how many signers can be added. Crossing the limit causes the earliest signer to be removed (FIFO). A user adding a signer may accidentally erase data created by their first signer. LIFO rules would be ideal, but this isn't possible without reliable timestamps. When an fid transfers custody addresses, the signature chain from address to fid to signer is broken. Hubs will discard unsigned messages after a short delay, unless the new address submits a signature, which could cause unintentional data loss. There are mitigations to these problems — a compaction can solve (1) and an approach like multi-signatures can make (2) bearable. But these solutions come at the cost of storage and additional complexity, and do not solve the problem entirely. Specification We propose tracking signers onchain with a Key Registry which maps fids to signers. The registry is extensible so that we can change the kinds of keys we use for signers or add keys for other purposes like encryption. When a key is added, the KeyRegistry checks the IdRegistry and assigns the keys to the caller's fid. Mapping keys to the fid instead of the custody address solves (2) by maintaining the relationship when the fid is transferred. Storing keys onchain lets hubs avoid limits since the blockchain implements its own fee system, avoiding (1). Four inputs are needed to register a new key pair to a caller: key - the public key material keyType- the type of key material provided metadata - additional information about the key request metadataType - the type of metadata being provided Keys are stateful and exist in the states null, added or removed. The owner may add and remove keys at any time, but removed keys can never be re-added. The same key can be added by other fids and may exist in different states across fids. A sample implementation of the Key Registry can be seen in Appendix A. Validation The Key Registry must validate the key and metadata by calling a validation contract which returns a boolean. Each tuple of (keyType, metadataType) encodes validation rules in a contract registered with the Key Registry. Each validation contract must have a validate method that requires: fid - the fid of the user registering the key key - the public key material metadata - information about the request The owner can add and update the validation contracts at any time. A sample interface for the validator can be seen in Appendix B. Key Types 1. Ed25519 A valid public key on Curve25519 used to sign Farcaster messages on behalf of an fid. The key is not validated onchain but is validated by Farcaster Hubs. Metadata Types 1. Signed Key Request A signature that proves that an fid (requesting fid) asked another fid (primary fid) to authorize a key pair. For example, if an app asks a user for a key it would sign a message proving that it made the ask. This lets us attribute signers to specific entities which is useful for a wide range of things from knowing which apps are being used to filtering content based on the applications that generated them. This type-specific metadata must be an ABI-encoded struct with the following format: struct SignedKeyRequest { uint256 requestingFid; address requestSigner; bytes signature; } Where requestingFid is the fid requesting the key, requestSigner is the owner of requestingFid, and signature is an EIP-712 signature with the following format: SignedKeyRequest(uint256 userFid,uint256 requestingFid,bytes signerPubKey) Hub Changes Hubs must implement KeyRegistrySignerStore which tracks currently active keys for each fid. When the KeyRegistry emits an event for a key where keyType ==1 and metadataType == 1, it is added or removed from the store. The existing SignerStore CRDT to track SignerAdd/SignerRemove messages is deprecated. The Hubs also expose APIs for this new functionality and deprecate unused APIs which are described in Appendix C. The exact process for transitioning from SignerStore to KeyRegistrySignerStore is described in Release. The General Rules for CRDTs are simplified to: Messages must have an Ed25519 signature scheme. Messages must be signed by a Signer for the message's fid tracked by the KeyRegistrySignerStore. Changes in the KeyRegistry may cause a Signer to be removed for an fid, which should revoke all messages created by that signer for that fid. Rationale Will the additional cost deter users from adding applications? It will likely cost between $.1 and $1 to add a signer at today’s L2 gas prices, though we expect that to reduce over time. Wallet clients like Warpcast can simplify the user experience by allowing payments to be made via fiat methods or L1. They can reduce the friction to add signers by charging a flat “signup fee” which covers the cost of signers being added later. Why can’t users re-add the same signer? The only use case for this is re-adding an accidentally deleted signer. In such cases, its more secure to issue a new signer and re-sign the messages. Disallowing signer reuse also simplifies sync logic for Hubs. Why is the smart contract not upgradeable? An upgradeable contract would allow changing logic without having to re-initialize the contract. However, it reduces trust since an administrator can upgrade the contract, change the logic, add keys to a user and forge messages under their identity. Why can’t a signer be revoked without deleting the messages created by it? A user revoking a signer typically expects that the owner of the signer can no longer create messages. However, this won’t be true since new messages created with an incorrect, older timestamp before the revocation will be accepted. Allowing this will lead to confusion, and its preferable to create a new signer and resign messages if they need to be preserved. Why not have counterfactual signers that exist off-chain and on-chain? A counterfactual system is possible but at the cost of significant complexity. The likely outcome in such a case is that wallets and apps will design their experiences around the off-chain signers, resulting in all the same problems that we face today. Release The Key Registry is deployed to the L2 on 0x00000000fc9e66f1c6d86d750b4af47ff0cc343d. Hubs listen for events emitted by this contract and track signers in a SmartContractSignerStore During the broader L2 migration, the Key Registry is seeded with all known Signers from the Hubs. Once complete, a migration timestamp is set onchain after which Hubs will reject offchain Signers produced with a timestamp ≥ the migration timestamp. An administrator can add or remove signers to this contract for 24 hours, used to clean up any inconsistencies that have arisen. Another release of the Hubs will deprecate the Signer CRDT and all offchain Signer messages. Appendix A: Key Registry // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.19; import {IdRegistry} from "./IdRegistry.sol"; contract KeyRegistry is Ownable2Step { /** * @notice State enumeration for a key in the registry. During migration, an admin can change * the state of any fids key from NULL to ADDED or ADDED to NULL. After migration, an * fid can change the state of a key from NULL to ADDED or ADDED to REMOVED only. * * - NULL: The key is not in the registry. * - ADDED: The key has been added to the registry. * - REMOVED: The key was added to the registry, but is now removed. */ enum KeyState { NULL, ADDED, REMOVED } /** * @notice Data about a key. * * @param state The current state of the key. * @param scheme The manner in which the key should be used. */ struct KeyData { KeyState state; uint200 scheme; } /*////////////////////////////////////////////////////////////// ERRORS //////////////////////////////////////////////////////////////*/ /// @dev Revert if a key violates KeyState transition rules. error InvalidState(); /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ /** * @dev Emit an event when an admin or fid adds a new key. * * @param fid The fid associated with the key. * @param scheme The type of the key. * @param key The key being registered. (indexed as hash) * @param keyBytes The bytes of the key being registered. * @param metadata Metadata about the key. */ event Add(uint256 indexed fid, bytes indexed key, bytes keyBytes, uint200 indexed scheme, bytes metadata); /** * @dev Emit an event when an fid removes an added key. * * @param fid The fid associated with the key. * @param key The key being registered. (indexed as hash) * @param keyBytes The bytes of the key being registered. */ event Remove(uint256 indexed fid, bytes indexed key, bytes keyBytes); /** * @dev Emit an event when the admin sets a metadata validator contract for a given * scheme and typeId. * * @param scheme The key scheme associated with this validator. * @param typeId The metadata typeId associated with this validator. * @param oldValidator The previous validator contract address. * @param newValidator The new validator contract address. */ event SetValidator(uint32 scheme, uint8 typeId, address oldValidator, address newValidator); /*////////////////////////////////////////////////////////////// CONSTANTS //////////////////////////////////////////////////////////////*/ /** * @dev Period in seconds after migration during which admin can bulk add/reset keys. * Admins can make corrections to the migrated data during the grace period if necessary, * but cannot make changes after it expires. */ uint24 public immutable gracePeriod; /*////////////////////////////////////////////////////////////// STORAGE //////////////////////////////////////////////////////////////*/ /** * @dev The IdRegistry contract. */ IdRegistry public immutable idRegistry; /** * @dev Timestamp at which keys migrated. Hubs will cut over to use this key registry as their * source of truth after this timestamp. */ uint40 public keysMigratedAt; /** * @dev Mapping of fid to a key to the key's metadata. * * @custom:param fid The fid associated with the key. * @custom:param key Bytes of the key. * @custom:param data Struct with the state and key type. */ mapping(uint256 fid => mapping(bytes key => KeyData data)) public keys; /** * @dev Mapping of scheme to metadata typeId to validator contract. * * @custom:param scheme Numeric key scheme. * @custom:param typeId Metadata typeId. * @custom:param validator Validator contract implementing IMetadataValidator. */ mapping(uint32 scheme => mapping(uint8 typeId => IMetadataValidator validator)) public validators; /*////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ /** * @notice Set the IdRegistry, migration grace period, and owner. * * @param _idRegistry IdRegistry contract address. Immutable. * @param _gracePeriod Migration grace period in seconds. Immutable. * @param _owner Contract owner address. */ constructor(address _idRegistry, uint24 _gracePeriod, address _owner) { _transferOwnership(_owner); gracePeriod = _gracePeriod; idRegistry = IdRegistry(_idRegistry); } /*////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////*/ /** * @notice Revert if caller does not own the associated fid. */ modifier onlyFidOwner(uint256 fid) { if (idRegistry.idOf(msg.sender) != fid) revert Unauthorized(); _; } /*////////////////////////////////////////////////////////////// VIEWS //////////////////////////////////////////////////////////////*/ /** * @notice Retrieve state and type data for a given key. * * @param fid The fid associated with the key. * @param key Bytes of the key. * * @return KeyData struct that contains the state and scheme. */ function keyDataOf(uint256 fid, bytes calldata key) external view returns (KeyData memory) { return keys[fid][key]; } /*////////////////////////////////////////////////////////////// REGISTRATION //////////////////////////////////////////////////////////////*/ /** * @notice Add a key to an fid, setting the key state to ADDED. * * @param fid The fid associated with the key. Caller must own the fid. * @param scheme The key's numeric scheme. * @param key Bytes of the key to add. * @param metadata Metadata about the key, which is not stored and only emitted in an event. */ function add(uint256 fid, uint200 scheme, bytes calldata key, bytes calldata metadata) external onlyFidOwner(fid) { _add(fid, scheme, key, metadata); // calls the validator } /** * @notice Remove a key associated with an fid, setting the key state to REMOVED. * The key must be in the ADDED state. * * @param fid The fid associated with the key. Caller must own the fid. * @param key Bytes of the key to remove. */ function remove(uint256 fid, bytes calldata key) external onlyFidOwner(fid) { _remove(fid, scheme, key, metadata); } } Appendix B: Validator interface IMetadataValidator { function validate(uint256 userFid, bytes memory signerKey, bytes memory metadata) external returns (bool); } Appendix C: Hub API Changes The following APIs calls will be added: message OnChainEventResponse { repeated OnChainEvent events = 1; } enum OnChainEventType { EVENT_TYPE_NONE = 0; EVENT_TYPE_SIGNER = 1; EVENT_TYPE_SIGNER_MIGRATED = 2; EVENT_TYPE_ID_REGISTER = 3; EVENT_TYPE_STORAGE_RENT = 4; } message OnChainEvent { OnChainEventType type = 1; uint32 chain_id = 2; uint32 block_number = 3; bytes block_hash = 4; uint64 block_timestamp = 5; bytes transaction_hash = 6; uint32 log_index = 7; uint64 fid = 8; oneof body { SignerEventBody signer_event_body = 9; SignerMigratedEventBody signer_migrated_event_body = 10; IdRegisterEventBody id_register_event_body = 11; StorageRentEventBody storage_rent_event_body = 12; } } message SignerEventBody { bytes key = 1; uint32 scheme = 2; SignerEventType event_type = 3; bytes metadata = 4; } rpc GetOnChainSigner(SignerRequest) returns (OnChainEvent); rpc GetOnChainSignersByFid(FidRequest) returns (OnChainEvent); // Returns only active signers rpc GetOnChainEvents(OnChainEventRequest) returns (OnChainEventResponse); // Can be used to get all on chain events (e.g. including signer removal events) The following API calls will be deprecated: rpc GetSigner(SignerRequest) returns (Message); rpc GetSignersByFid(FidRequest) returns (MessagesResponse); rpc GetAllSignerMessagesByFid(FidRequest) returns (MessagesResponse); // submitMessage will no longer accept SignerAdd or SignerRemove, which will be deprecated