The following standard provides a mechanism by which smart contracts can route storage to external providers. In particular, protocols can reduce the gas fees associated with storing data on mainnet by routing the handling of storage operations to another system or network. These storage routers act as an extension to the core L1 contract. Methods in this document specifically target security and cost-effectiveness of storage routing to three router types: L1, L2 and databases. The cross-chain data written with these methods can be retrieved by generic EIP-3668-compliant contracts, thus completing the cross-chain data life cycle. This document, nicknamed CCIP-Store, alongside EIP-3668, is a meaningful step toward a secure infrastructure for cross-chain storage routers and data retrievals.
EIP-3668, aka 'CCIP-Read', has been key to retrieving cross-chain data for a variety of contracts on Ethereum blockchain, ranging from price feeds for DeFi contracts, to more recently records for ENS users. The latter case dedicatedly uses cross-chain storage to bypass the usually high gas fees associated with on-chain storage; this aspect has a plethora of use cases well beyond ENS records and a potential for significant impact on universal affordability and accessibility of Ethereum.
Cross-chain data retrieval through EIP-3668 is a relatively simpler task since it assumes that all relevant data originating from cross-chain storages is translated by CCIP-Read-compliant HTTP gateways; this includes L2 chains and databases. On the flip side however, so far each service leveraging CCIP-Read must handle writing this data securely to these storage types on their own, while also incorporating reasonable security measures in their CCIP-Read-compatible contracts for verifying this data on L1. While these security measures are in-built into L2 architectures, database storage providers on the other hand must incorporate some form of explicit security measures during storage operations so that cross-chain data's integrity can be verified by CCIP-Read contracts during data retrieval stage. Examples of this include:
In this context, a specification which allows storage routing to external routers will facilitate creation of services that are agnostic to the underlying storage solution. This in turn enables new applications to operate without knowledge of the underlying routers. This 'CCIP-Store' proposal outlines precisely this part of the process, i.e. how the bespoke storage routing can be made by smart contracts to L2s and databases.
The following specification revolves around the structure and description of a cross-chain storage router tasked with the responsibility of writing to an L2 or database storage. This document introduces StorageRoutedToL2() and StorageRoutedToDatabase() storage routers, along with the trivial StorageRoutedToL1() router, and proposes that new StorageRoutedTo__() reverts be allowed through new EIPs that sufficiently detail their interfaces and designs. Some foreseen examples of new storage routers include StorageRoutedToSolana() for Solana, StorageRoutedToFilecoin() for Filecoin, StorageRoutedToIPFS() for IPFS, StorageRoutedToIPNS() for IPNS, StorageRoutedToArweave() for Arweave, StorageRoutedToArNS() for ArNS, StorageRoutedToSwarm() for Swarm etc.
StorageRoutedToL1()A minimal L1 router is trivial and only requires the L1 contract address to which routing must be made, while the clients must ensure that the calldata is invariant under routing to another contract. One example implementation of an L1 router is given below.
In this example, the routing must prompt the client to build the transaction with the exact same original calldata, and submit it to the L1 contract by calling the exact same function.
StorageRoutedToL2()A minimal L2 router only requires the list of chainId values and the corresponding L2 contract addresses, while the clients must ensure that the calldata is invariant under routing to L2. One example implementation of an L2 router in an L1 contract is shown below.
In this example, the routing must prompt the client to build the transaction with the exact same original calldata, and submit it to the L2 by calling the exact same function on L2 as L1.
StorageRoutedToDatabase()A minimal database router is similar to an L2 in the sense that:
a) Similar to chainId, it requires the gatewayUrl that is tasked with handling off-chain storage operations, and
b) Similar to eth_call, it requires eth_sign output to secure the data, and the client must prompt the users for these signatures.
This specification does not require any other data to be stored on L1 other than the bespoke gatewayUrl; the storage router therefore should only return the gatewayUrl in revert.
Following the revert, the client must take these steps:
Request the user for a secret signature sigKeygen to generate a deterministic dataSigner keypair,
Sign the calldata with generated data signer's private key and produce verifiable data signature dataSig,
Request the user for an approval approving the generated data signer, and finally,
Post the calldata to gateway along with signatures dataSig and approval, and the dataSigner.
These steps are described in detail below.
The data signer must be generated deterministically from ethereum wallet signatures; see figure below.
The deterministic key generation can be implemented concisely in a single unified keygen() function as follows.
This keygen() function requires three variables: username, spice and sigKeygen. Their definitions are given below.
usernameCAIP-10 identifier username is auto-derived from the connected wallet's checksummed address wallet and chainId using EIP-155.
spicespice is calculated from the optional private field password, which must be prompted from the user by the client; this field allows users to change data signers for a given username.
Password must then be stretched before use with PBKDF2 algorithm such that:
where pepper = keccak256(abi.encodePacked(username)) and the iterations count is fixed to 500,000 for brute-force vulnerability protection.
sigKeygenThe data signer must be derived from the owner or manager keys of a node. Message payload for the required sigKeygen must then be formatted as:
where the extradata is calculated as follows,
The remaining protocol field is a protocol-specific identifier limiting the scope to a specific protocol represented by a unique contract address. This identifier cannot be global and must be uniquely defined for each implementating L1 contract such that:
With this deterministic format for signature message payload, the client must prompt the user for the ethereum signature. Once the user signs the messages, the keygen() function can derive the data signer keypair.
Since the derived signer is wallet-specific, it can
simultaneously in the background without ever prompting the user. Signature(s) dataSig accompanying the off-chain calldata must implement the following format in their message payloads:
where dataType parameters are protocol-specific and formatted as object keys delimited by /. For instance, if the off-chain data is nested in keys as a > b > c > field > key, then the equivalent dataType is a/b/c/field/key. For example, in order to update off-chain ENS record text > avatar and address > 60, dataType must be formatted as text/avatar and address/60 respectively.
The dataSigner is not stored on L1, and the clients must instead
approval signature for dataSigner signed by the owner or manager of a node, andapproval and the dataSigner along with the signed calldata in encoded form.CCIP-Read-enabled contracts can then verify during resolution time that the approval attached with the signed calldata comes from the node's manager or owner, and that it approves the expected dataSigner. The approval signature must have the following message payload format:
where dataSigner must be checksummed.
The final EIP-3668-compatible data payload in the off-chain data file is identified by a fixed callback.signedData.selector equal to 0x2b45eb2b and must follow the format
The client must construct this data and pass it to the gateway in the POST request along with the raw values for indexing. The CCIP-Read-enabled contracts after decoding the four parameters from this data must
dataSigner is approved by the owner or manager of the node through approval, anddataSig is produced by dataSignerbefore resolving the encodedData value in decoded form.
POST RequestThe POST request made by the client to the gatewayUrl must follow the format as described below.
Example of a complete Post typed object for updating multiple ENS records for a node is shown below.
Each new storage router must submit their StorageRoutedTo__() identifier through an ERC track proposal referencing the current document.
Each StorageRoutedTo__() provider must be supported with detailed documentation of its structure and the necessary metadata that its implementers must return.
Each StorageRoutedTo__() proposal must define the precise formatting of any message payloads that require signatures and complete descriptions of custom cryptographic techniques implemented for additional security, accessibility or privacy.
ENS off-chain resolvers capable of reading from and writing to databases are perhaps the most common use-case for CCIP-Read and CCIP-Write. One example of such a (minimal) resolver is given below along with the client-side code for handling the storage router revert.
Technically, the cases of L2s and databases are similar; routing to an L2 involves routing the eth_call to another EVM, while routing to a database can be made by extracting eth_sign from eth_call and posting the resulting signature explicitly along with the data for later verification. Methods in this document perform these precise tasks when routing storage operations to external routers. In addition, methods such as signing data with a derived signer (for databases) allow for significant UX improvement by fixing the number of signature prompts in wallets to 2, irrespective of the number of data instances to sign per node or the total number of nodes to update. This improvement comes at no additional cost to the user and allows services to perform batch updates.
None
Clients must purge the derived signer private keys from local storage immediately after signing the off-chain data.
Signature message payload and the resulting deterministic signature sigKeygen must be treated as a secret by the clients and immediately purged from local storage after usage in the keygen() function.
Clients must immediately purge the password and spice from local storage after usage in the keygen() function.
Copyright and related rights waived via CC0.