L1SLOAD el nuevo opcode para Keystores seguras y escalables
Published: 2024-06-12 20:18:56

---
title: L1SLOAD el nuevo opcode para Keystores seguras y escalables
published: true
description: 
tags: 
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-06-06 15:00 +0000
---

Las funciones de abstracción de cuentas cross-chain serán posibles gracias a los [Keystores](https://notes.ethereum.org/@vbuterin/minimal_keystore_rollup). Los usuarios podrán controlar varias smart contracts wallets, en múltiples chains, con una sola llave. Esto puede traer la tan esperada buena experiencia de usuario para los usuarios finales en [los rollups de Ethereum](https://ethereum-magicians.org/t/a-rollup-centric-ethereum-roadmap/4698).

Para que esto suceda, necesitamos poder leer los datos de L1 desde los rollups en L2, lo cual actualmente es un proceso muy costoso. Es por eso que Scroll introdujo recientemente el precompile `L1SLOAD` que es capaz de leer el estado de L1 de manera rápida y económica. Safe wallet ha creado un demo presentado en Safecon Berlín 2024. Pienso que esto es solo el comienzo, esto podrá mejorar aplicaciones cross-chain en DeFi, juegos, redes sociales y muchos más.

Vamos ahora a aprender, con ejemplos prácticos, los conceptos básicos de esta nueva primitiva que está abre la puerta a una nueva forma de interactuar con Ethereum.

## 1. Conecta tu wallet al Scroll Devnet

Actualmente, `L1SLOAD` está disponible únicamente en la Scroll Devnet. Toma nota y no la confundas con la Scroll Sepolia Testnet. Aunque ambos están desplegados sobre el Sepolia Testnet, son cadenas separadas.

Comenzamos conectando nuestra wallet a la Scroll Devnet:

* Name: `Scroll Devnet`
* RPC: `https://l1sload-rpc.scroll.io`
* Chain id: `222222`
* Symbol: `Sepolia ETH`
* Explorer: `https://l1sload-blockscout.scroll.io`

![Connect to Scroll Devnet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4myzwxva7wdp6j2vskcl.png)

## 2. Obtener fondos en Scroll Devnet

Existen dos métodos de obtener fondos en la Scroll Devnet. Elige el que prefieras.

{% collapsible Bot de Faucet en Telegram (recomendado) %}

Únete a [este grupo de telegram](https://t.me/scroll_l1sload_devnet_bot) y escribe `/drop TUADDRESS` (e.g. `/drop 0xd8da6bf26964af9d7eed9e03e53415d37aa96045`) para recibir fondos directo a tu cuenta.

{% endcollapsible %}

{% collapsible Bridge de Sepolia %}

Puedes enviar fondos de Sepolia a la Scroll Devnet a través del bridge. Existen dos maneras de lograr esto pero en este caso usaremos Remix.

Conectemos ahora tu wallet con Sepolia ETH a Sepolia Testnet. Recuerda que puedes obtener Sepolia ETH grátis en [un faucet](https://sepoliafaucet.com/),

Ahora compila la siguiente interfaz.

```js
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

interface ScrollMessanger {
    function sendMessage(address to, uint value, bytes memory message, uint gasLimit) external payable;
}
```

A continuación, en la tab de "Deploy & Run" conecta el contrato siguiente: `0x561894a813b7632B9880BB127199750e93aD2cd5`.

![Connect Scroll Messenger Interface on Remix](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/opjs8r2ojy7epjmijjbw.png)

Ahora puedes enviar ETH llamando la función `sendMessage` como se detalla a continación:

* to: La dirección de tu cuenta EOA. El address que recibirá fondos en L2.
* value: La cantidad de ether que deseas recibir en L2, en formato wei. Por ejemplo, si envías `0.01` ETH debes pasar como parámetro `10000000000000000`
* message: Déjalo en blanco, simplemente envía `0x00`
* gasLimit: `1000000` debería ser suficiente

Also remember to pass some value to your transaction. And add some extra ETH to pay for fees on L2, `0.001` should be more than enough. So if for example you sent `0.01` ETH on the bridge, send a transaction with `0.011` ETH to cover the fees.

También recuerda pasar un extra `value` en tu transacción. Es decir, agrega un poco de ETH extra para pagar las comisiones en L2, `0.001` debería ser más que suficiente. Así que, por ejemplo, si enviaste 0.01 ETH en el bridge, envía una transacción con `0.011` ETH para cubrir las comisiones.

![Send ETH from Sepolia to Scroll Devnet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zwkdvhyt23j4k9i2x8e4.png)

{% endcollapsible %}

Haz click en el botón `transact` y tus fondos deberían llegar en 15 mins aproximadamente.

## 2. Lanza tu contrato en L2

Tal y como mencionamos anteriormente, `L1SLOAD` lee el estado de contratos en L1 desde L2. Lancemos ahora un contrato simple en L1 que luego leeremos el valor de la variable `number` desde L2.

```js
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 */
contract L1Storage {

    uint256 public number;

    /**
     * @dev Store value in variable
     * @param num value to store
     */
    function store(uint256 num) public {
        number = num;
    }

    /**
     * @dev Return value
     * @return value of 'number'
     */
    function retrieve() public view returns (uint256){
        return number;
    }
}
```

Ahora llamamos `store(uint256 num)` pasándole un nuevo valor para la variable `number`. Por ejemplo, le podemos pasar `42`.

![Store a value on L1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0oecx64a2vthd8mjsx5l.png)

## 3. Obtener una slot desde L2

Lanzamos el siguiente contrato en L2 pasando el address del contrato que recién lanzamos en L1 como parámetro en el constructor.

```js
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

interface IL1Blocks {
    function latestBlockNumber() external view returns (uint256);
}

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    uint256 constant NUMBER_SLOT = 0;
    address immutable l1StorageAddr;

    uint public l1Number;

    constructor(address _l1Storage) {
        l1StorageAddr = _l1Storage;
    }

    function latestL1BlockNumber() public view returns (uint256) {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        return l1BlockNum;
    }

    function retrieveFromL1() public {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        bytes memory input = abi.encodePacked(l1BlockNum, l1StorageAddr, NUMBER_SLOT);
        bool success;
        bytes memory ret;
        (success, ret) = L1_SLOAD_ADDRESS.call(input);
        if (success) {
            (l1Number) = abi.decode(ret, (uint256));
        } else {
            revert("L1SLOAD failed");
        }
    }
}
```

Observa que este contrato primero llama `latestL1BlockNumber()` para obtener el más reciente bloque en L1 que está disponible para lectura en L2. Luego, llamamos `L1SLOAD` (opcode `0x101`) pasando el address del contrato en L1 como parámetro y la slot 9 que es donde la variable `number` está ubicada dentro de ese contrato.

Ahora podemos llamar `retrieveFromL1()` para obtener el valor almacenado previamente.

![L2SLOAD L1 State red from L2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p92y2kkjb9ecvtcv32re.png)

## Ejemplo #2: Leer otros tipos de variables

Solidity guarda las slots de las variables en el mismo orden en la que fueron declaradas. Esto es bastante conveniente para nosotros. Por ejemplo, en el siguiente contrato, `account` se almacena en la slot #0, `number` en la #1 y `text` en la #2.

```js
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract AdvancedL1Storage {
    address public account;
    uint public number;
    string public text;
}
```

Podemos observar que cómo podemos obtener los valores de diferentes tipos: uint256, address, etc... Las strings son un poco diferente por la naturaleza variable de su tamaño.

```js
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

interface IL1Blocks {
    function latestBlockNumber() external view returns (uint256);
}

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    address immutable l1ContractAddress;

    address public account;
    uint public number;
    string public test;

    constructor(address _l1ContractAddress) { //0x5555158Ea3aB5537Aa0012AdB93B055584355aF3
        l1ContractAddress = _l1ContractAddress;
    }

    // Internal functions

    function latestL1BlockNumber() internal view returns (uint256) {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        return l1BlockNum;
    }

    function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
        bool success;
        bytes memory returnValue;
        (success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
        if(!success)
        {
            revert("L1SLOAD failed");
        }
        return returnValue;
    }

    function decodeStringSlot(bytes memory encodedString) internal pure returns (string memory) {
        uint length = 0;
        while (length < encodedString.length && encodedString[length] != 0x00) {
            length++;
        }
        bytes memory data = new bytes(length);
        for (uint i = 0; i < length; i++) {
            data[i] = encodedString[i];
        }
        return string(data);
    }

    // Public functions

    function retrieveAddress() public {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
    }

    function retrieveNumber() public {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
    }

    function retrieveString() public {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
    }

    function retrieveAll() public {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
        number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
        test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
    }
}
```

## Ejemplo #3: Leer el balance de un token ERC20 en L1

Comenzamos lanzando un token ERC20 bastante sencillo.

```js
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract SimpleToken is ERC20 {
    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply * 1 ether);
    }
}
```

A continuación, lanzamos el siguiente contrato en L2 pasando como parámetros el address del token que recién lanzamos en L1.

```js
// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

interface IL1Blocks {
    function latestBlockNumber() external view returns (uint256);
}

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    address immutable l1ContractAddress;

    uint public l1Balance;

    constructor(address _l1ContractAddress) {
        l1ContractAddress = _l1ContractAddress;
    }

    // Internal functions
    function latestL1BlockNumber() public view returns (uint256) {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        return l1BlockNum;
    }

    function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
        bool success;
        bytes memory returnValue;
        (success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
        if(!success)
        {
            revert("L1SLOAD failed");
        }
        return returnValue;
    }

    // Public functions
    function retrieveL1Balance(address account) public {
        uint slotNumber = 0;
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        l1Balance = abi.decode(retrieveSlotFromL1(
            l1BlockNum,
            l1ContractAddress,
            uint(keccak256(
                abi.encodePacked(uint160(account),slotNumber)
                )
            )
            ), (uint));
    }
}
```

Los contratos de OpenZeppelin colocan convenientemente el mapping de [los balances del token en el Slot 0](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L30). Así que puedes llamar a `retrieveL1Balance()` pasando el address del holder como parámetro y el balance del token se almacenará en la variable `l1Balance`. Como puedes ver en el código, el proceso es primer convertir el address a uint160 y luego lo hasheamos con el slot del mapping, que es 0. Esto se debe a que es así como Solidity implementa los mappings.

**¡Gracias por leer esta guía!**

Sígueme en dev.to y en [Youtube](https://www.youtube.com/channel/UCNRB4tgwp09z4391JRjEsRA) para todo lo relacionado al desarrollo en Blockchain en Español.