---
eip: 7700
title: Cross-chain Storage Router Protocol
description: Provides a mechanism to replace L1 storage with L2 and databases through cross-chain routers
author: Avneet Singh (@sshmatrix), 0xc0de4c0ffee (@0xc0de4c0ffee), Nick Johnson (@arachnid), Makoto Inoue (@makoto)
discussions-to: https://ethereum-magicians.org/t/erc-7700-cross-chain-storage-router-protocol/19853
status: Draft
type: Standards Track
category: ERC
created: 2024-04-30
requires: 155
---

## Abstract
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](./eip-3668)-compliant contracts, thus completing the cross-chain data life cycle. This document, nicknamed CCIP-Store, alongside [EIP-3668](./eip-3668), is a meaningful step toward a secure infrastructure for cross-chain storage routers and data retrievals.

## Motivation
[EIP-3668](./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](./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:

- Services that allow the management of namespaces, e.g. ENS domains, stored externally on an L2 solution or off-chain database as if they were native L1 tokens, and,
- Services that allow the management of digital identities stored on external storages as if they were stored in the native L1 smart contract.

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. 

![Fig.1 CCIP-Store and CCIP-Read Workflows](../assets/eip-7700/images/Schema.svg)

## Specification
### Overview
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.

### L1 Router: `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.

```solidity
// Define revert event
error StorageRoutedToL1(
    address contractL1
);

// Generic function in a contract
function setValue(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    // Get metadata from on-chain sources
    (
        address contractL1, // Routed contract address on L1; may be globally constant
    ) = getMetadata(node); // Arbitrary code
    // contractL1 = 0x32f94e75cde5fa48b6469323742e6004d701409b
    // Route storage call to L1 router
    revert StorageRoutedToL1( 
        contractL1
    );
};
```

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.

```solidity
// Function in routed L1 contract
function setValue(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    // Some code storing data mapped by node & msg.sender
    ...
}
```

![Fig.2 L1 Call Lifecycle](../assets/eip-7700/images/L1.svg)

### L2 Router: `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.

```solidity
// Define revert event
error StorageRoutedToL2(
    address contractL2, 
    uint256 chainId
);

// Generic function in a contract
function setValue(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    // Get metadata from on-chain sources
    (
        address contractL2, // Contract address on L2; may be globally constant
        uint256 chainId // L2 ChainID; may be globally constant
    ) = getMetadata(node); // Arbitrary code
    // contractL2 = 0x32f94e75cde5fa48b6469323742e6004d701409b
    // chainId = 21
    // Route storage call to L2 router
    revert StorageRoutedToL2( 
        contractL2,
        chainId
    );
};
```

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.

```solidity
// Function in L2 contract
function setValue(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    // Some code storing data mapped by node & msg.sender
    ...
}
```

![Fig.3 L2 Call Lifecycle](../assets/eip-7700/images/L2.svg)

### Database Router: `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.

```solidity
error StorageRoutedToDatabase(
    string gatewayUrl
);

// Generic function in a contract
function setValue(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    (
        string gatewayUrl // Gateway URL; may be globally constant
    ) = getMetadata(node);
    // gatewayUrl = "https://api.namesys.xyz"
    // Route storage call to database router
    revert StorageRoutedToDatabase( 
        gatewayUrl
    );
};
```

![Fig.4 Database Call Lifecycle](../assets/eip-7700/images/Database.svg)

Following the revert, the client must take these steps:

1. Request the user for a secret signature `sigKeygen` to generate a deterministic `dataSigner` keypair,

2. Sign the calldata with generated data signer's private key and produce verifiable data signature `dataSig`,

3. Request the user for an `approval` approving the generated data signer, and finally,

4. Post the calldata to gateway along with signatures `dataSig` and `approval`, and the `dataSigner`.

These steps are described in detail below.

#### 1. Generate Data Signer
The data signer must be generated deterministically from ethereum wallet signatures; see figure below.

![Fig.5 Data Signer Keygen Workflow](../assets/eip-7700/images/Keygen.svg)

The deterministic key generation can be implemented concisely in a single unified `keygen()` function as follows.

```js
/* Pseudo-code for key generation */
function keygen(
  username, // CAIP identifier for the blockchain account
  sigKeygen, // Deterministic signature from wallet
  spice // Stretched password
) {
  // Calculate input key by hashing signature bytes using SHA256 algorithm
  let inputKey = sha256(sigKeygen);
  // Calculate salt for keygen by hashing concatenated username, stretched password (aka spice) and hex-encoded signature using SHA256 algorithm
  let salt = sha256(`${username}:${spice}:${sigKeygen}`);
  // Calculate hash key output by feeding input key, salt & username to the HMAC-based key derivation function (HKDF) with dLen = 42
  let hashKey = hkdf(sha256, inputKey, salt, username, 42);
  // Calculate and return secp256k1 keypair
  return secp256k1(hashKey); // Calculate secp256k1 keypair from hash key
}
```

This `keygen()` function requires three variables: `username`, `spice` and `sigKeygen`. Their definitions are given below.

##### 1. `username`
[CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/ad0cfebc45a4b8368628340bf22aefb2a5edcab7/CAIPs/caip-10.md) identifier `username` is auto-derived from the connected wallet's checksummed address `wallet` and `chainId` using [EIP-155](./eip-155).

```js
/* CAIP-10 identifier */
const caip10 = `eip155:${chainId}:${wallet}`;
```

##### 2. `spice`
`spice` 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`.
```js
/* Secret derived key identifier */ 
// Clients must prompt the user for this
const password = 'key1';
```

Password must then be stretched before use with `PBKDF2` algorithm such that:

```js
/* Calculate spice by stretching password */
let spice = pbkdf2(
            password, 
            pepper, 
            iterations
        ); // Stretch password with PBKDF2
```

where `pepper = keccak256(abi.encodePacked(username))` and the `iterations` count is fixed to `500,000` for brute-force vulnerability protection.

```js
/* Definitions of pepper and iterations in PBKDF2 */
let pepper = keccak256(abi.encodePacked(username));
let iterations = 500000; // 500,000 iterations
```

##### 3. `sigKeygen`
The 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:

```text
Requesting Signature To Generate Keypair(s)\n\nOrigin: ${username}\nProtocol: ${protocol}\nExtradata: ${extradata}
```

where the `extradata` is calculated as follows,

```solidity
// Calculating extradata in keygen signatures
bytes32 extradata = keccak256(
    abi.encodePacked(
        spice
        wallet
    )
)
```

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:

```js
/* Protocol identifier in CAIP-10 format */
const protocol = `eth:${chainId}:${contract}`;
```

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. 

#### 2. Sign Data
Since the derived signer is wallet-specific, it can 

- sign batch data for multiple keys for a given node, and 
- sign batches of data for multiple nodes owned by a wallet

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:  

```text
Requesting Signature To Update Off-Chain Data\n\nOrigin: ${username}\nData Type: ${dataType}\nData Value: ${dataValue}
```

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.
 
#### 3. Approve Data Signer
The `dataSigner` is not stored on L1, and the clients must instead

- request an `approval` signature for `dataSigner` signed by the owner or manager of a node, and
- post this `approval` 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:

```text
Requesting Signature To Approve Data Signer\n\nOrigin: ${username}\nApproved Signer: ${dataSigner}\nApproved By: ${caip10}
```

where `dataSigner` must be checksummed.

#### 4. Post CCIP-Read Compatible Payload
The final [EIP-3668](./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

```solidity
/* Compile CCIP-Read-compatible payload*/
bytes encodedData = abi.encode(['bytes'], [dataValue]); // Encode data
bytes funcSelector = callback.signedData.selector; // Identify off-chain data with a fixed 'signedData' selector = '0x2b45eb2b'
bytes data = abi.encode(
    ['bytes4', 'address', 'bytes32', 'bytes32', 'bytes'],
    [funcSelector, dataSigner, dataSig, approval, encodedData]
); // Compile complete CCIP-Readable off-chain data
```

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 

- verify that the `dataSigner` is approved by the owner or manager of the node through `approval`, and
- verify that the `dataSig` is produced by `dataSigner`

before resolving the `encodedData` value in decoded form.

##### `POST` Request
The `POST` request made by the client to the `gatewayUrl` must follow the format as described below.

```ts
/* POST request format*/
type Post = {
  node: string
  preimage: string
  chainId: number
  approval: string
  payload: {
    field1: {
      value: string
      signature: string
      timestamp: number
      data: string
    }
    field2: [
      {
        index: number
        value: string
        signature: string
        timestamp: number
        data: string
      }
    ]
    field3: [
      {
        key: number
        value: string
        signature: string
        timestamp: number
        data: string
      }
    ]
  }
}
```

Example of a complete `Post` typed object for updating multiple ENS records for a node is shown below.

```ts
/* Example of a POST request */
let post: Post = {
  node: "0xe8e5c24bb5f0db1f3cab7d3a7af2ecc14a7a4e3658dfb61c9b65a099b5f086fb",
  preimage: "dev.namesys.eth",
  chainId: 1,
  approval: "0xa94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c",
  payload: {
    contenthash: {
      value: "ipfs://QmYSFDzEcmk25JPFrHBHSMMLcTKLm6SvuZvKpijTHBnAYX",
      signature: "0x24730d1d85d556245b7766aef413188e22f219c8de263ccbfafee4413f0937c32e4f44068d84c7424f923b878dcf22184f8df86506de1cea3dad932c5bd5e9de1c",
      timestamp: 1708322868,
      data: "0x2b45eb2b000000000000000000000000fe889053f7a0d2571f1898d2835c3cbdf50d766b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004124730d1d85d556245b7766aef413188e22f219c8de263ccbfafee4413f0937c32e4f44068d84c7424f923b878dcf22184f8df86506de1cea3dad932c5bd5e9de1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000026e301017012209603ccbcef5c2acd57bdec6a63e8a0292f3ce6bb583b6826060bcdc3ea84ad900000000000000000000000000000000000000000000000000000"
    },
    address: [
      {
        coinType: 0,
        value: "1FfmbHfnpaZjKFvyi1okTjJJusN455paPH",
        signature: "0x60ecd4979ae2c39399ffc7ad361066d46fc3d20f2b2902c52e01549a1f6912643c21d23d1ad817507413dc8b73b59548840cada57481eb55332c4327a5086a501b",
        timestamp: 1708322877,
        data: "0x2b45eb2b000000000000000000000000fe889053f7a0d2571f1898d2835c3cbdf50d766b000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000004160ecd4979ae2c39399ffc7ad361066d46fc3d20f2b2902c52e01549a1f6912643c21d23d1ad817507413dc8b73b59548840cada57481eb55332c4327a5086a501b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000a0e6ca5444e4d8b7c80f70237f332320387f18c7"
      },
      {
        coinType: 60,
        value: "0x47C10B0491A138Ddae6cCfa26F17ADCfCA299753",
        signature: "0xaad74ddef8c031131b6b83b3bf46749701ed11aeb585b63b72246c8dab4fff4f79ef23aea5f62b227092719f72f7cfe04f3c97bfad0229c19413f5cb491e966c1b",
        timestamp: 1708322917,
        data: "0x2b45eb2b000000000000000000000000fe889053f7a0d2571f1898d2835c3cbdf50d766b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000041aad74ddef8c031131b6b83b3bf46749701ed11aeb585b63b72246c8dab4fff4f79ef23aea5f62b227092719f72f7cfe04f3c97bfad0229c19413f5cb491e966c1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000047c10b0491a138ddae6ccfa26f17adcfca299753"
      }
    ],
    text: [
      {
        key: "avatar",
        value: "https://namesys.xyz/logo.png",
        signature: "0xbc3c7f1b511de151bffe8df033859295d83d400413996789e706e222055a2353404ce17027760c927af99e0bf621bfb24d3bfc52abb36bcfbe6e20cf43db7c561b",
        timestamp: 1708329377,
        data: "0x2b45eb2b000000000000000000000000fe889053f7a0d2571f1898d2835c3cbdf50d766b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000041bc3c7f1b511de151bffe8df033859295d83d400413996789e706e222055a2353404ce17027760c927af99e0bf621bfb24d3bfc52abb36bcfbe6e20cf43db7c561b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001c68747470733a2f2f6e616d657379732e78797a2f6c6f676f2e706e6700000000"
      },
      {
        key: "com.github",
        value: "namesys-eth",
        signature: "0xc9c33ff219e90510f79b6c9bb489917ee6e00ab123c55abe1117e71ea0d171356cf316420c71cfcf4bd63a791aaf37388ef1832e582f54a8c2df173917240fff1b",
        timestamp: 1708322898,
        data: "0x2b45eb2b000000000000000000000000fe889053f7a0d2571f1898d2835c3cbdf50d766b0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000041c9c33ff219e90510f79b6c9bb489917ee6e00ab123c55abe1117e71ea0d171356cf316420c71cfcf4bd63a791aaf37388ef1832e582f54a8c2df173917240fff1b000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041a94da8233afb27d087f6fbc667cc247ef2ed31b5a1ff877ac823b5a2e69caa49069f0daa45a464d8db2f8e4e435250cb446d8f279d45a2b865ebf2fff291f69f1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b6e616d657379732d657468000000000000000000000000000000000000000000"
      }
    ]
  }
}
```

### New Revert Events
1. Each new storage router must submit their `StorageRoutedTo__()` identifier through an ERC track proposal referencing the current document.

2. Each `StorageRoutedTo__()` provider must be supported with detailed documentation of its structure and the necessary metadata that its implementers must return.

3. 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.

### Implementation featuring ENS on L2 & Database
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.

#### L1 Contract
```solidity
/* ENS resolver implementing StorageRoutedToDatabase() */
interface iResolver {
    // Defined in EIP-7700
    error StorageRoutedToL2(
        uint chainId,
        address contractL2
    );
    error StorageRoutedToDatabase(
        string gatewayUrl
    );
    // Defined in EIP-137
    function setAddr(bytes32 node, address addr) external;
}

// Defined in EIP-7700
string public gatewayUrl = "https://post.namesys.xyz"; // RESTful API endpoint
uint256 public chainId = uint(21); // ChainID of L2
address public contractL2 = "0x839B3B540A9572448FD1B2335e0EB09Ac1A02885"; // Contract on L2

/**
* Sets the ethereum address associated with an ENS node
* [!] May only be called by the owner or manager of that node in ENS registry
* @param node Namehash of ENS domain to update
* @param addr Ethereum address to set
*/
function setAddr(
    bytes32 node,
    address addr
) authorised(node) {
    // Route to database storage
    revert StorageRoutedToDatabase(
        gatewayUrl
    );
}

/**
* Sets the avatar text record associated with an ENS node
* [!] May only be called by the owner or manager of that node in ENS registry
* @param node Namehash of ENS domain to update
* @param key Key for ENS text record
* @param value URL to avatar
*/
function setText(
    bytes32 node,
    string key,
    string value
) external {
    // Verify owner or manager permissions
    require(authorised(node), "NOT_ALLOWED");
    // Route to L2 storage
    revert StorageRoutedToL2(
        chainId, 
        contractL2
    );
}
```

#### L2 Contract
```solidity
// Function in L2 contract
function setText(
    bytes32 node,
    bytes32 key,
    bytes32 value
) external {
    // Store record mapped by node & sender
    records[keccak256(abi.encodePacked(node, msg.sender))]["text"][key] = value;
}
```

#### Client-side Code
```ts
/* Client-side pseudo-code in ENS App */
// Deterministically generate signer keypair
let signer = keygen(username, sigKeygen, spice);
// Construct POST body by signing calldata with derived private key
let post: Post = signData(node, addr, signer.priv);
// POST to gateway
await fetch(gatewayUrl, {
  method: "POST",
  body: JSON.stringify(post)
});
```

## Rationale
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.

## Backwards Compatibility
None

## Security Considerations
1. Clients must purge the derived signer private keys from local storage immediately after signing the off-chain data.

2. 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.

3. Clients must immediately purge the `password` and `spice` from local storage after usage in the `keygen()` function.

## Copyright
Copyright and related rights waived via [CC0](../LICENSE.md).