Cross-Domain Messaging
Scroll tiene un bridge para el paso de mensajes arbitrarios que permite la transferencia de tokens y permite a las dapps comunicarse entre la capa 1 y la capa 2. Esto significa que las dapps de la capa 1 pueden activar funciones de contrato en la capa 2, y viceversa. A continuación, explicaremos cómo se transmiten los mensajes entre la capa 1 y la capa 2.
Envío de mensajes de L1 a L2

Existen dos enfoques principales para enviar un mensaje de L1 a L2: enviar mensajes arbitrarios a través de L1ScrollMessenger
y enviar transacciones forzadas a través de EnforcedTxGateway
. Ambos enfoques permiten a los usuarios iniciar una transacción L2 en L1 y llamar a contratos arbitrarios en L2. En el caso de los mensajes arbitrarios, el remitente de las transacciones L2 es la dirección alias “L1ScrollMessenger”. Para las transacciones forzadas, el remitente de L2 es una cuenta de titularidad externa (EOA). Además, proporcionamos varias gateways de tokens estándares para facilitar a los usuarios el depósito de ETH y otros tokens estándares, incluidos ERC-20, ERC-677, ERC-721 y ERC-1155. En esencia, estas gateways codifican los depósitos de tokens en un mensaje y lo envían a sus homólogos en L2 a través del contrato L1ScrollMessenger
. Puede encontrar más detalles sobre las gateways de tokens de L1 en Gateways de Depósito.
Como se muestra en la Figura 1, tanto los mensajes arbitrarios como las transacciones forzadas se añaden a la cola de mensajes almacenada en el contrato L1MessageQueue
. El contrato L1MessageQueue
proporciona dos funciones appendCrossDomainMessage
y appendEnforcedTransaction
para añadir mensajes arbitrarios y transacciones forzadas respectivamente.
/// @notice Append an arbitrary L1-to-L2 message into this contract./// @param target The target address on L2./// @param gasLimit The maximum gas can be used for this transaction on L2./// @param data The calldata of the L1-initiated transaction.function appendCrossDomainMessage( address target, uint256 gasLimit, bytes calldata data) external;
/// @notice Append an enforced transaction to this contract./// @param sender The sender address of this transaction./// @param target The target address of this transaction./// @param value The value to be transferred on L2./// @param gasLimit The maximum gas should be used for this transaction on L2./// @param data The calldata of the L1-initiated transaction.function appendEnforcedTransaction( address sender, address target, uint256 value, uint256 gasLimit, bytes calldata data) external;
Ambas funciones construyen una transacción iniciada por L1 con un nuevo tipo de transacción L1MessageTx
introducido en la Scroll chain y calculan el hash de la transacción (ver más detalles en Transacción de Mensajes en L1). A continuación, L1MessageQueue
añade el hash de la transacción a la cola de mensajes y emite el evento QueueTransaction(sender, target, value, queueIndex, gasLimit, calldata)
. La diferencia entre appendCrossDomainMessage
y appendEnforcedTransaction
a la hora de construir transacciones de mensajes L1 es:
appendCrossDomainMessage
sólo puede ser llamado porL1ScrollMessenger
y utiliza la Address Alias demsg.sender
, que será la dirección deL1ScrollMessenger
, como remitente de la transacción.- La función
appendEnforcedTransaction
sólo puede ser llamada porEnforcedTxGateway
y utilizasender
del parámetro de función como remitente de la transacción. Esto permite a los usuarios ejecutar una retirada o transferencia de ETH desde sus cuentas L2 directamente a través del bridge L1.
Después de que la transacción se ejecuta con éxito en L1, el watcher en el secuenciador Scroll que monitoriza el contrato L1MessageQueue
recoge los nuevos eventos QueueTransaction
de los bloques en L1. El secuenciador construye entonces una nueva transacción L1MessageTx
por evento y las añade a su cola local de transacciones en L1. Cuando construye un nuevo bloque en L2, el secuenciador incluye las transacciones tanto de su cola de transacciones en L1 como de su mempool en L2. Ten en cuenta que las transacciones de mensajes en L1 deben incluirse secuencialmente basándose en el orden de la cola de mensajes en L1 en el contrato L1MessageQueue
. Las transacciones L1MessageTx
siempre van primero en los bloques en L2 seguidas de las transacciones en L2. Actualmente, limitamos el número de transacciones L1MessageTx
en un bloque en L2 a NumL1MessagesPerBlock
(fijado actualmente en 10).
A continuación, nos extenderemos más en el proceso específico de envío de mensajes arbitrarios a través de L1ScrollMessenger
y el envío de transacciones forzadas a través de EnforcedTxGateway
.
Envío de Mensajes Arbitrarios
El contrato L1ScrollMessenger
proporciona dos funciones sendMessage
para enviar mensajes arbitrarios. La única diferencia es que la segunda permite a los usuarios especificar una dirección de reembolso distinta de la del remitente para recibir el reembolso de la comisión.
sendMessage
signatura de funciones
sendMessage
signatura de funciones/// @param target The target address on L2./// @param value The value to deposit to L2 from `msg.value`./// @param message The message passed to target contract./// @param gasLimit The maximum gas can be used for this transaction on L2.function sendMessage( address target, uint256 value, bytes memory message, uint256 gasLimit) external payable;
/// @param target The target address on L2./// @param value The value to deposit to L2 from `msg.value`./// @param message The message passed to target contract./// @param gasLimit The maximum gas can be used for this transaction on L2./// @param refundAddress The address to refund excessive fee on L1.function sendMessage( address target, uint256 value, bytes calldata message, uint256 gasLimit, address refundAddress) external payable;
Ambas funciones requieren que los usuarios proporcionen un límite de gas para la transacción L1MessageTx
correspondiente en L2 y paguen por adelantado el Message Relay Fee en L1, que se calcula en función del importe del límite de gas. La comisión se recoge en un contrato feeVault
en L1. En caso de que la transacción falle en L2 porque el usuario no ha establecido el límite de gas correcto para su mensaje en L1, el usuario puede repetir el mismo mensaje con un límite de gas más alto. Puedes encontrar más detalles en la sección Reintento de Mensajes Fallidos, pero dado que la porción de gas de comisiones que no fue usada es devuelta al usuaior, hoy hay penalidad por sobrestimar el límite de gas.
Las funciones sendMessage
codifican los argumentos en un mensaje entre dominios (véase el fragmento de código siguiente), donde el nonce del mensaje es el siguiente índice de la cola de mensajes en L1. Los datos codificados se utilizan como calldata en la transacción L1MessageTx
ejecutada en L2. Ten en cuenta que estos mensajes entre dominios siempre llaman a la función relayMessage
del contrato L2ScrollMessenger
en L2.
abi.encodeWithSignature( "relayMessage(address,address,uint256,uint256,bytes)", _sender, _target, _value, _messageNonce, _message)
La cantidad de ETH depositada como value
se bloquea en el contrato L1ScrollMessenger
. Si la cantidad de ETH en el mensaje no puede cubrir la Message Relay Fee y la cantidad depositada, la transacción se revertirá. El contrato L1ScrollMessenger
reembolsará la cantidad sobrante a la refundAddress
designada o al remitente de la transacción en caso contrario. Por último, L1ScrollMessenger
añade el mensaje entre dominios a L1MessageQueue
mediante el método appendCrossDomainMessage
.
Envío de Transacciones Forzadas
El contrato EnforcedTxGateway
proporciona dos funciones sendTransaction
para enviar una transacción forzada. En la primera función, el remitente de la transacción generada L1MessageTx
es el remitente de la transacción. Por otro lado, la segunda función utiliza la dirección del sender
como remitente de la transacción L1MessageTx
. Esto permite a un tercero enviar una transacción forzada en nombre del usuario y pagar la comisión de retransmisión. Ten en cuenta que la segunda función requiere proporcionar una firma válida de la transacción L1MessageTx
generada que coincida con la dirección del sender
. Ambas funciones sendTransaction
exigen que el remitente sea una cuenta EOA.
sendTransaction
signatura de funciones
sendTransaction
signatura de funciones/// @param target The target address on L2./// @param value The value to withdraw from the `tx.origin` address on L2./// @param gasLimit The maximum gas can be used for this transaction on L2./// @param data The calldata passed to target contract.function sendTransaction( address target, uint256 value, uint256 gasLimit, bytes calldata data) external payable;
/// @param sender The sender address who will initiate this transaction on L2./// @param target The target address on L2./// @param value The value to withdraw from the `sender` address on L2./// @param gasLimit The maximum gas can be used for this transaction on L2./// @param data The calldata passed to target contract./// @param signature The signature for the corresponding `L1MessageTx` transaction./// @param refundAddress The address to refund excessive fee on L1.function sendTransaction( address sender, address target, uint256 value, uint256 gasLimit, bytes calldata data, bytes memory signature, address refundAddress) external payable;
De forma similar a la retransmisión arbitraria de mensajes, sendTransaction
deduce la comisión de retransmisión de mensajes y la transfiere a la cuenta feeVault
de L1. Pero una diferencia clave es que el value
que se pasa a la función indica la cantidad de ETH que debe transferirse desde la cuenta del remitente en L2, no en L1. Por lo tanto, el msg.value
sólo necesita cubrir el Message Relay Fee. Si la cantidad de ETH en el mensaje no puede cubrir la comisión, la transacción fallará. Cualquier exceso de comisión se reembolsa al remitente de la transacción en la primera función y a la refundAddress
en la segunda función. Por último, EnforcedTxGateway
llama a L1MessageQueue.appendEnforcedTransaction
para añadir la transacción a la cola de mensajes.
Reintento de Mensajes Fallidos
Si una transacción de L1MessageTx
falla en L2 debido a gas insuficiente, los usuarios pueden reproducir el mensaje con un límite de gas más alto. Para ello, L1ScrollMessenger
proporciona el método replayMessage
, que permite a los usuarios enviar la misma información que el mensaje anterior fallido con un límite de gas superior. Este mensaje se convertirá en una nueva transacción L1MessageTx
en L2. Ten en cuenta que no se reembolsará la tarifa de gas de la transacción fallida anterior, ya que ésta ya se ha procesado en L2.
replayMessage
signatura de función
replayMessage
signatura de función/// @param from The address of the sender of the message./// @param to The address of the recipient of the message./// @param value The msg.value passed to the message call./// @param queueIndex The queue index for the message to replay./// @param message The content of the message./// @param newGasLimit New gas limit to be used for this message./// @param refundAddress The address of account who will receive the refunded fee.function replayMessage( address from, address to, uint256 value, uint256 queueIndex, bytes memory message, uint32 newGasLimit, address refundAddress) external payable;
Dado que el contrato L2ScrollMessenger
registra todos los mensajes de L1 que se retransmitieron con éxito a L2, la transacción del mensaje reproducido se revertirá en L2 si el mensaje original tiene éxito.
Message Relay Fee
El contrato L2GasPriceOracle
desplegado en L1 calcula la comisión de retransmisión de un mensaje dado su límite de gas. Este contrato almacena el valor l2BaseFee
en su memoria, que se actualiza mediante un relayer dedicado ejecutado actualmente por Scroll. La comisión de retransmisión de los mensajes de L1 a L2 es gasLimit * l2BaseFee
.
Address Alias
Debido al comportamiento del opcode CREATE
, es posible que alguien despliegue un contrato en la misma dirección en L1 y L2 pero con diferente bytecode. Para evitar que usuarios malintencionados se aprovechen de esto, el bridge aplica un Address Alias cuando el remitente del mensaje es un contrato en L1. La dirección del remitente alias de la transacción del mensaje L1 es l1_dirección_del_contrato + offset
donde el offset
es 0x11110000000000000000000000000000000000001111
.
Sending Messages from L2 to L1

En L2, los usuarios pueden enviar mensajes arbitrarios a través de L2ScrollMessenger
para retirar tokens y llamar a contratos L1. Al igual que en L1, hemos creado varias gateways de tokens estándares para facilitar la inicialización de los retiros de tokens. Para más detalles sobre las gateways de tokens de L2, consulta Gateways de Retiro.
El contrato L2ScrollMessenger
también proporciona una función sendMessage
. La diferencia con respecto a L1ScrollMessenger.sendMessage
es que el parámetro gasLimit
se ignora en la función porque la transacción de ejecución de retiro en L1 es enviada por los usuarios y la comisión de transacción se paga en L1 directamente. Así, la función sendMessage
requiere que msg.value
sea igual al parámetro value
. La función codifica los argumentos en un mensaje entre dominios siguiendo el mismo esquema que en L1ScrollMessenger
.
sendMessage
signatura de función
sendMessage
signatura de función/// @param target The target address on L1./// @param value The value to withdraw to L1 from `msg.value`./// @param message The message passed to target contract./// @param _gasLimit Ignored in the L2ScrollMessenger because the withdrawal execution on L1 is done by the user.function sendMessage( address target, uint256 value, bytes memory message, uint256 _gasLimit) external payable;
A continuación, el hash de mensajes entre dominios se añade a L2MessageQueue
llamando a su función appendMessage
. El contrato L2MessageQueue
mantiene el Withdraw Trie, un Merkle tree sólo para añadir mensajes. Cada vez que se añade un nuevo mensaje a la cola, el contrato lo inserta en el Withdraw Trie y actualiza el hash root del trie.
Una vez finalizado el lote de transacciones que contiene los mensajes L2 a L1 de los usuarios en el contrato L1 rollup, los usuarios deben enviar las transacciones Execute Withdrawal correspondientes para llamar al método relayMessageWithProof
del contrato L1ScrollMessenger
que ejecuta la retirada en L1. Gracias a las Merkle proofs, la finalización de las transacciones de retirada en L1 no genera confianza y puede ser enviada por el propio usuario o por un tercero en nombre de los usuarios.
Para facilitar la construcción de un MIP de retiro, Scroll mantiene un servicio llamado Bridge History API. Bridge History API supervisa los eventos SentMessage
emitidos por L2ScrollMessenger
y mantiene internamente un Withdraw Trie. Genera continuamente Merkle proofs para cada mensaje de retirada. Los usuarios y los servicios de terceros pueden consultar las Merkle proofs desde la Bridge History API para incluirlas en las transacciones Execute Withdrawal.
Ten en cuenta que las transacciones de ejecución de retirada pueden ser enviadas por los propios usuarios o por un servicio de terceros.
Withdraw Trie

El Withdraw Trie es un “Merkle tree” binario denso. El hash de un nodo hoja se hereda del hash del mensaje, mientras que el hash de un nodo no-hoja es el Keccak hash digest de los hashes concatenados de sus dos descendientes. La profundidad del Withdraw Trie crece dinámicamente en función del número de mensajes añadidos al trie.
La figura 3(a) muestra un ejemplo de un withdraw trie completo de 3 capas. Cuando el número de hojas no puede saturar un tree binario completo, rellenamos los nodos de las hojas con hash 0, como se muestra en las figuras 3(b) y 3(c). Cuando se añade un nuevo mensaje a un Withdraw Trie no completo, el nodo de relleno se sustituye por un nuevo nodo hoja con el hash del mensaje real.