Sequencer Level Security (SLS)

Overview

Zircuit will protect users at the sequencer level by monitoring the mempool for malicious transactions and preventing their inclusion into a block. In comparison to typical security efforts that focus on the application and smart contract levels, Zircuit’s revolutionary approach goes directly to the underlying sequencer level.

This approach is called "Sequencer Level Security" (SLS) and adds an extra layer of security to the Zircuit network. This novel approach means that our sequencer scrutinizes transactions for potential malicious intent before they are finalized on layer 2. By enabling early detection and quarantine of suspicious transactions, the SLS protocol enhances the security of smart contracts and the layer 2 without necessitating the contentious measures of hard forks or block reversion.

Detailed Description

Zircuit adds another layer of security at the sequencer level. The sequencer is a privileged node that collects users’ transactions, similar to Ethereum, and also orders them based on predefined rules.

The figure above presents an overview of the protocol. It contains three main components: (1) Malice Detection, (2) Quarantine-Release Criterion, and (3) Transaction Execution.

Upon arrival at the SLS sequencer, transactions from the mempool are initially routed to Malice Detection module. In our talks, we refer to this module as the “oracle.” It identifies whether a transaction is benign or potentially malicious. Benign transactions are promptly queued for block inclusion, adhering to standard sequencing protocols. Conversely, transactions flagged as malicious are diverted to the Quarantine-Release Criterion module, which acts as an intermediary holding area. Here, they undergo a rigorous verification process against specific release criteria. Transactions that meet these criteria are then forwarded to the Transaction Execution module. The Transaction Execution module executes the transactions against the blockchain state at the forthcoming L2 block. Successfully executed transactions are cycled back to the SLS sequencer for inclusion in the forthcoming L2 block.

Malice Detection

Malice Detection is done via the following steps:

  1. Choosing Transactions by the Sequencer: The sequencer selects a list of transactions for potential inclusion in the upcoming block. This step is the same as other standard sequencing protocols. This includes both transactions from the mempool, and deposit transactions that have origin on L1.

  2. Parallel Simulation on the Tip of the Chain: Each transaction is independently simulated using the current state at the tip of the blockchain. This step allows for parallel processing of transactions. The outcomes of these simulations provide essential data for future dependency analysis and malice detection: (1) Simulation results of each transaction (2) The blockchain states read and written by each transaction.

  3. Transaction Dependency Analysis: We perform the analysis on the state read and written by each transaction and identify the dependencies between all transactions. Informally, one transaction is dependent on another if executing one may change the outcome of executing the other.

  4. Parallel Detection for Independent Transactions and Sequential Detection for Dependent Transactions: For any transaction that is not dependent (a.k.a. independent) on any other prior transaction, the sequencer can perform parallel detection on their simulation results. Other dependent transactions are queued for sequential simulation and detection within the block context.

  5. Transaction Inclusion: The sequencer finalizes the block by including all transactions identified as benign. Dependent transactions that could not be fully analyzed due to time constraints or complexity are deferred to the next cycle. The same detection process will be applied in the next round when these transactions are considered again for inclusion.

The algorithms used by the sequencer to identify malicious include program analysis, machine learning, and rule-based methods.

Quarantine-Release Criterion

While in quarantine, the transaction does not get executed and cannot be included in the blocks. The sequencer maintains the information about when the transaction has been placed in the quarantine. The transaction will either be dropped from the mempool once it meets one of the retirement criteria or be released from the quarantine if it meets one of the release criteria.

The exact retirement criteria and release criteria can be defined by the sequencer.

Mempool Retirement Criteria:

  • Nonce criterion. This criterion is met if the transaction can no longer be included in a block because the nonce is no longer valid.

  • Time criterion and memory constraints. This criterion is met if the transaction clutters the mempool that the node maintains. Such a transaction can be resubmitted to the network and re-enter the mempool (in the quarantined state).

The current implementation of Zircuit ensures that transactions in quarantine are being checked periodically on the retirement criteria by the sequencer. The following list of quarantine-release criteria are viable from the security standpoint.

Release Criteria:

  • Time criterion. The time criterion represents the reaction time that the sequencer offers to the users to react to a malicious transaction. If the transaction has been quarantined for longer than required, it can be released and considered for block inclusion. The exact amount of time required for the transaction to stay in the quarantine is a configuration parameter.

  • Failure criterion. If a transaction fails due to changes in the chain’s state, it can be safely included in the block since it will result in a revert. Reverted transactions do not alter the blockchain state.

  • Administrative criterion. It is expected that the detection of malice will occasionally produce false positives. Under such circumstances, the sequencer operational team, comprising security experts, can administratively override decisions to release transactions.

We refer to the Sequencer-Level Security paper for other possible release criteria.

At the launch time, the Zircuit sequencer uses only the Time criterion set to a very long period of time (in the order of years). During this period, the sequencer waits for a privileged party to trigger the release criteria. Contact us on discord for assistance to claim quarantined deposits.

Transaction Execution

Upon releasing from the quarantine, the sequencer can consider including the transaction when forming the next block. This is subject to the regular sequencing Rules. For a transaction that was resubmitted due to being originally underpriced for the current chain state, the transaction should not be quarantined again. The SLS protocol has to use the account address, and the transaction data (the function selector and call data), and value, to establish whether a new incoming transaction is a duplicate of a transaction that has been already released from the quarantine.

Requirements for Builders

The SLS protocol is implemented in Zircuit natively. Therefore, every smart contract deployed on Zircuit is by default included and protected by the SLS. However, SLS is a best-effort service powered by AI, so it can make mistakes. The developers are therefore encouraged to follow best engineering practices and scrutinize the security of their code. The sequencer-level security protocol should be considered an added secondary security measure.

SLS requires pricing information for detecting malice and protecting assets. Tokens natively deployed on Zircuit should be listed on CoinGecko. The technology will automatically recognize such price feeds and tokens. Tokens that are deployed on other networks and bridged to Zircuit are advised to contact the Zircuit team via Discord to ensure rapid inclusion in the oracle’s pricing system.

Building with SLS

To build with SLS, we added two additional RPC calls to the Geth client, zirc_isQuarantined as well as zirc_getQuarantined.

zirc_isQuarantined takes a transaction hash as a parameter and outputs the quarantine status of the transaction. The RPC call can be done through any tool of choice, such as Foundry's cast:

cast rpc zirc_isQuarantined 
--rpc-url https://zircuit1-mainnet.p2pify.com/
 0x9b629147b75dc0b275d478fa34d97c5d4a26926457540b15a5ce871df36c23fd 

Using zirc_getQuarantined, you can also query all quarantined transactions and filter them by addresses.

cast rpc zirc_getQuarantined 
--rpc-url https://zircuit1-mainnet.p2pify.com/ 
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

A quick overview of quarantined transactions is also available on the Quarantined Transactions page on the block explorer.

It's important to note that a transaction that gets quarantined will also not return a transaction receipt, so if you are used to sending a transaction using the Ethers library and waiting for a receipt, but the transaction was put into quarantine, the wait() call will not terminate.

const sentTx = await signer.sendTransaction(txBody);
console.log('Transaction sent:', sentTx.hash);
// Waiting for transaction receipt, if TX gets quarantined this will time out
// and quarantine status can be checked through zirc_isQuarantined RPC call
const receipt = await sentTx.wait();

In the more advanced code snippet below, we proactively check the quarantine status using the zirc_isQuarantined using two concurrent promises to either wait for a transaction receipt or a RPC call result, that the transaction is in fact quarantined.

let continueChecking = true;

async function isQuarantined(txHash) {
    const response = await provider.send('zirc_isQuarantined', [txHash]);
    return response.IsQuarantined;
}

// Continuously check quarantine status (10 second timeout)
async function checkQuarantine(txHash) {
    while (continueChecking) {
        if (await isQuarantined(txHash)) {
            throw new Error('Transaction is quarantined.');
        }
        await new Promise(resolve => setTimeout(resolve, 10000)); // Check every 10 seconds
    }
}

// Main function to send TX
async function sendTransaction() {
    try {
        const txResponse = await wallet.sendTransaction(tx);
        console.log('Transaction sent:', txResponse.hash);

        // Wait for either the transaction to be mined or for it to be quarantined
        const result = await Promise.race([
            txResponse.wait().then(receipt => {
                continueChecking = false; // Stop the quarantine check to prevent infinite promise
                return receipt;
            }), // Waits for the transaction to be mined
            checkQuarantine(txResponse.hash) // Continuously checks if transaction is quarantined
        ]);

        console.log('Transaction mined:', result);
    } catch (error) {
        continueChecking = false; // Ensure the check is stopped in case of errors too
        console.error('Error:', error.message);
    }
}

sendTransaction();

If your transaction is quarantined, refer to the mempool retirement and release criteria mentioned earlier. Security experts may release false positives (Administrative criterion), but you can also attempt to resubmit the transaction with a different gas limit to reevaluate its status. If you are sending the exact same transaction without any changes you will get an already known error response, so a simple retry is not enough to remove a false positive from the quarantine.

In cases where a quarantined transaction is blocking subsequent ones—such as due to a nonce mismatch—you can resolve the issue by sending a zero-value transaction to yourself. This action can update the nonce and help release the quarantined transaction.

You can perform this manually using MetaMask or programmatically using Ethereum libraries like Cast or Ethers.js.

cast send 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 
--value 0ether 
--rpc-url https://zircuit1-mainnet.p2pify.com/ 
--private-key $yourPrivateKey

Using ethers, assuming you have set up a signer using RPC URL and private key.

const tx = {
  to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  value: ethers.utils.parseEther("0"),
  gasLimit: 21000, // Standard gas limit for a basic transaction
};

try {
  const transaction = await signer.sendTransaction(tx);
  console.log(`Transaction hash: ${transaction.hash}`);
  await transaction.wait();
  console.log('Transaction confirmed');
} catch (error) {
  console.error('Error sending transaction:', error);
}

Learn More

Technical details can be found in our pre-print:

You can also learn more by watching some of our talks on the topic:

  • our talk at ETH Prague 2024,

  • our talk at Ethereum Zurich 2024, or

  • our talk at ETH Denver 2024.