Skip to content

Syncing to L2

Keyspace's state root can be trustlessly synced to L2 chains, even chains that aren't directly supported by the Keyspace team. If the network has a trusted source of L1 block hashes or beacon roots, the state root can be synced to L2 without depending on the Keyspace sequencer.

Merkle Proofs

We expect L2s to sync to Keyspace's state root using Merkle proofs of the L1 KeyStore contract's storage because the proofs are currently cheaper than deposit transactions on L1.

Block Hash Proofs

Rollups typically implement a block hash oracle to verify the state root of the L1 chain. The oracle can be used to verify the state root of the L1 KeyStore contract, which can then be used to verify the state root of the L2 KeyStore contract.

function setRoot(
    bytes memory blockHeaderRlp,
    bytes[] memory accountProof,
    bytes[] memory slotProof) external
{
    BlockHeader memory header = parseBlockHeader(blockHeaderRlp);
    if (header.hash != l1Block.hash()) {
        revert BlockHashMismatch(l1Block.hash(), header.hash);
    }
 
    // MerkleTrie.get reverts if the slot does not exist.
    bytes32 accountHash = keccak256(abi.encodePacked(l1KeyStore));
    bytes memory accountFields = MerkleTrie.get({
        _key: abi.encodePacked(accountHash),
        _proof: accountProof,
        _root: header.stateRootHash
    });
    bytes32 storageRoot = bytes32(accountFields.toRlpItem().toList()[2].toUint());
 
    uint256 slotValue = MerkleTrie.get({
        _key: abi.encodePacked(slotHash),
        _proof: slotProof,
        _root: storageRoot
    }).toRlpItem().toUint();
 
    root = uint256(bytes32(slotValue));
    roots.append(root);
    emit RootUpdated(root, header.number, header.hash);
}

Beacon Root Proofs

OP Stack rollups implement EIP 4788, which provides a buffer of beacon roots for approximately the past day. Instead of proving against rapidly changing L1 block hashes, the L2 can prove against the beacon roots, which are cached by the protocol for a longer period of time.

function setRoot(
    bytes32 stateRoot,
    bytes32[] calldata stateRootProof,
    bytes[] calldata accountProof,
    bytes[] calldata slotProof,
    uint64 beaconTimestamp) external
{
    if (beaconTimestamp <= lastBeaconTimestamp) {
        revert BeaconTimestampDecreased(lastBeaconTimestamp, beaconTimestamp);
    }
    lastBeaconTimestamp = beaconTimestamp;
    
    bytes32 beaconRoot = getBeaconBlockRoot(beaconTimestamp);
    if (!SSZ.verifyProof(stateRootProof, beaconRoot, stateRoot, STATE_ROOT_GINDEX)) {
        revert InvalidStateRootProof(stateRoot, beaconRoot, beaconTimestamp);
    }
 
    // MerkleTrie.get reverts if the slot does not exist.
    bytes32 accountHash = keccak256(abi.encodePacked(l1KeyStore));
    bytes memory accountFields = MerkleTrie.get({
        _key: abi.encodePacked(accountHash),
        _proof: accountProof,
        _root: stateRoot
    });
    bytes32 storageRoot = bytes32(accountFields.toRlpItem().toList()[2].toUint());
 
    uint256 slotValue = MerkleTrie.get({
        _key: abi.encodePacked(slotHash),
        _proof: slotProof,
        _root: storageRoot
    }).toRlpItem().toUint();
 
    root = uint256(bytes32(slotValue));
    roots.append(root);
    emit RootUpdated(root, beaconTimestamp, stateRoot);
}

Deposit Transactions

Deposit transactions are the most straightforward way to sync state to L2, but they currently cost about 10 times the cost of verifying a Merkle proof of the same storage on L2.

function syncL2() external {
    bytes memory data = abi.encodeCall(IKeyStoreL2.setRoot, (keyStore.root()));
    basePortal.depositTransaction(baseKeyStore, 0, 50_000, false, data);
    optimismPortal.depositTransaction(optimismKeyStore, 0, 50_000, false, data);
}