Fact

Relic Protocol's goal is not simply to prove and store facts about historical blockchain state, but to empower applications to easily access this information. Therefore, Relic Protocol uses a unique architecture that makes accessing this information simple.

Reliquary

The main component of this architecture is the Reliquary. The Reliquary is a smart contract which is responsible for storing facts, as well as controlling how facts are verified for storage.
Facts are stored in a two-tiered mapping: the first level is the account with which the fact is associated, and the second is the fact signature for that particular fact. We will talk more about this shortly.

mapping(address => mapping(FactSignature => bytes)) internal provenFacts;

Accounts

Each fact is associated with an account. Items like account age or account storage data are stored in a mapping specific to that account. In this manner, it is easy to check if a certain account has already proven a piece of data.
Some facts are special and do not have an associated account. For example: Ethereum block headers do not have an associated account. These types of facts are simply associated with the null address.

Fact Signatures

To disambiguate facts in the Reliquary, each class of fact is given a unique signature. These are constructed by taking a Keccak-256 hash of parameters used to describe the fact that is stored. One byte of the 32-byte hash is then used to indicate any fees that may be associated with querying the fact.
For example, the so-called "birth block" fact which describes a past block in which the account is proven to have existed is constructed as keccak256("BirthCertificate") << 8 | 0. This indicates the fact class (BirthCertificate), fact parameters (in this case empty), and fee for querying the fact (free).

A more complicated example is a storage slot fact, which is parametrized by the block and storage slot. The signature in this case is keccak256("StorageSlot", slot, blockNumber) << 8 | 0. This indicates the fact class (AccountStorage), fact parameters (the storage slot and the block number), and again no fee for querying.

Of course, developers are not expected to handle this logic themselves for constructing these signatures, instead they can use the Relic Protocol SDK to handle the details on Solidity or in TypeScript.

Fact Data

Each fact has additional data associated with it. At minimum, this data is simply the unique identifier of the prover responsible for proving the fact and storing it in the Reliquary. However, additional data may be stored depending on the type of fact. For example: account age facts store the block number and block timestamp of the corresponding block, meanwhile storage slot facts store the value contained in the storage slot.

Because the data stored varies so greatly, there is no universal way to interpret it. Instead, functions are provided by the SDK which can assist with extracting the values contained in the Reliquary. For example, the BirthCertificateVerifier contract contains functions to retrieve the unpacked block number and timestamp associated with the account. As storage slot facts contain the raw storage slot data, their interpretation depends on the contract from which the data was retrieved.

Some facts contain no extra data. For example, AccountStorage provers will create a fact parametrized by the block number and storage root, but will not store any additional data. These facts can be used to verify data sent on chain very efficiently: if an AccountStorage fact with a specific block number and storage root exist, we know those pieces of information were proven.

Proving Facts

The actual proof of each fact is performed using a combination of Merkle-Patricia Trie proofs and the block headers proven and stored by Relic Protocol. Most proofs follow the format that is obtained from a normal Ethereum node, however, Relic Protocol's Client SDK provides easy interfaces to create these proofs.

Facts can be proven by interfacing directly with the provers, which is facilitated by the Client SDK. However, depending on the circumstances there may be efficiency gains by instead having a dApp call the proof. This is especially true for ephemeral facts.

It is possible to re-prove data for an existing fact. The details depend on the specific prover, but in most cases even if data for a particular account and fact signature already exist, a request to re-prove that data will succeed. This may be done for a couple of reasons: it’s possible the original prover used to store the fact signature was deemed untrustworthy, and so a user wishes to prove it with a better one; or the user may wish to refine an existing proof, perhaps a user had proven they owned at least one of a token, but wishes to refine this to state they owned at least 10.

Ephemeral Facts

While the Reliquary enabled facts to be stored for future use, in many instances this is not ideal. Storing facts requires at least one, and possibly more storage slot writes, each costing 20k gas. For simple use cases wishing to be as efficient as possible and when it is unlikely a proof will be redone, Relic Protocol enables a fact to be proven without being stored. To make use of ephemeral facts, a dApp can simply accept the proof data as an argument and use Relic Protocol to verify the proof.

Querying Facts

To retrieve facts stored in the Reliquary, there are a few helper functions provided by the Relic Protocol Solidity SDK. As discussed earlier, facts have an account and a fact signature associated with them, and therefore both of those are required to query facts. Fact retrieval requires much less gas than the initial proof, making commonly accessed pieces of data such as account age very simple to query.

The main functions provided by the SDK for querying data are VerifyFact which will return any data associated with a fact, assuming it exists; and VerifyFactVersion which will simply return the prover ID used to prove the fact, assuming it exists.