CIP-110: Ceramic Anchor Contract Source

Author Sergey Ukustov
Discussions-To https://github.com/ceramicnetwork/CIP/issues/110
Status Final
Category RFC
Created 2021-10-14

Simple Summary

We can open other anchor service operators to participate in Ceramic anchoring using an allowlist as a smart contract on Ethereum blockchain.

Abstract

This improvement proposal presents a Ceramic Anchor Contract. It is a novel way to make anchors on Ethereum network.

Motivation

As of today, anchoring on Ceramic is done independently by each Ceramic Anchor Service (CAS). An anchor is a transaction from a wallet to itself with data, that represent a CID of a Merkle root.

While being low cost, such a construction presents a security risk, especially for Ceramic mainnet. A malicious CAS might withhold IPFS data necessary for proving an anchor (i.e. Merkle tree elements put on IPFS), thus making a Ceramic stream effectively invalid. We would like to make anchor services available for running by anybody, but this risk we would like to mitigate. Thus, until a validator network is in place, we would like to have a known set of trusted anchor services. We would like though the list to be open for anybody to join.

Specification

We can achieve that using a smart contract on Ethereum network (as our main network used for anchoring) that contains an allowlist of trusted anchor services. Only an allowed CAS can make an anchor transaction. The allowlist can be modified, preferrable by a DAO.

Contract Features

  • contract should be “ownable”
    • an owner can transfer ownership to other address
    • this allows us to start with just a single externally-owned address as an owner, and later transfer ownership to a DAO
  • contract should maintain an allowlist of allowed CAS accounts
    • only the contract owner can modify the allowlist
    • “modify” means “add” or “remove”
    • the contract should emit appropriate events for both adding and removal
    • an allowed address can remove itself
  • contract has “anchor” function
    • only an allowed address can call the function
    • the function should accept a CID of the merkle root
    • the function should emit an appropriate event

Implementation details

The contract should be written in Solidity language, using Truffle, Hardhat, or Foundry toolboxes. It might use OpenZeppelin library.

For “ownable” feature we are going to extend our contract Ownable from OpenZeppelin. The functionality provided there is good enough, so we do not elaborate on that topic further.

The allowlist is maintained as mapping (address => bool). We use two functions to modify the allowlist: addCas and removeCas, both emit a respective event:

mapping (address => bool) allowList;

event DidAddCas(address indexed _service);
event DidRemoveCas(address indexed _service);

// Only owner can add to the allowList
function addCas(address _service) public onlyOwner {
    allowList[_service] = true;
    emit DidAddCas(_service);
}
    
// Removal can be performed by the owner or the service itself
function removeCas(address _service) public {
    require((owner() == _msgSender()) || (allowList[_msgSender()] && _msgSender() == _service), "Caller is not allowed or the owner");
    delete allowList[_service];
    emit DidRemoveCas(_service);
}

The “anchorDagCbor” function can be called by an allowed service only. The single parameter root contains bytes (i.e. without multibase prefix) of an anchor Merkle root’s CID. The function’s responsibility is limited to just emit an event:

// Only an address in the allowlist is allowed.
modifier onlyAllowed() {
    require(
            ( allowList[ msg.sender ] || msg.sender == owner() ), 
            "Allow List: caller is not allowed");
    _;
}

event DidAnchor(address indexed _service, bytes32 _root);

// Here _root is a byte representation of Merkle root CID.
function anchorDagCbor(bytes32 _root) public onlyAllowed {
    emit DidAnchor(msg.sender, _root);
}

Anchoring Process

We maintain an authoritative list of anchoring contracts across Ethereum networks: Ropsten, Rinkeby, mainnet. This list is shared by CAS and Ceramic nodes.

CAS should use a contract on a network of choice for anchoring. When validating an anchor record, Ceramic node should check if a transaction happens on the authoritative contract, and the emitted event contains a merkle root.

Backwards Compatibility

Contract-based anchoring is not backwards compatible. We have to delineate between version of the anchors. We add a version property to AnchorCommit payload. Contract-based anchors use version: 1. If version is omitted, we expect the proof to be a raw transaction, and we check if it originated from one of the old well-known CAS addresses. After some time (as block time), to be defined later when CIP is accepted, Ceramic network only allows version: 1 proofs.

Rationale

An allowlist is for CAS instances, it allows them to do blockchain transactions. Thus, it is natural, that it is implemented as a blockchain smart contract.

Mainly we use Ethereum network now for anchoring, so Ethereum-based allowlist is a natural choice. It does not prevent to use other blockchains in future.

Single Ethereum transaction could be considered expensive. With the outlined design we effectively equalize the costs through multiple co-existing CAS instances.

Implementation

Ceramic Anchor Service

Security Considerations

Ideally, the contract should be independently audited to mitigate smart contract risks.

We consider allowed CAS to behave well, which is only enforced on social consensus layer until fully decentralized validators network is in place.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Sergey Ukustov, "CIP-110: Ceramic Anchor Contract," Ceramic Improvement Proposals, no. 110, October 2021. [Online serial]. Available: https://github.com/ceramicnetwork/CIPs/blob/main/CIPs/cip-110.md