> ## Documentation Index
> Fetch the complete documentation index at: https://docs.optimism.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Transaction flow

> Learn the transaction flow process for rollup transactions.

<Info>
  **Learn the OP Stack — stop 6 of 13.**
  You've deployed a chain and started its components. This page replays
  what you just ran as the life of a single transaction, from submission
  to finality on Ethereum. When you're done, continue to
  [Transaction fees on OP Mainnet](/op-stack/transactions/fees).
</Info>

This guide explains the transaction flow process for rollup transactions. The process for a rollup transaction has two requirements.

* The transaction needs to be written to L1 (Ethereum).
  This is typically performed by `op-batcher`, but any user can send an L1 transaction to submit an L2 transaction, in which case `op-batcher` is bypassed.

* The transaction needs to be executed to modify the state (by the execution client, `op-reth` by default).
  Afterwards, `op-proposer` writes a [commitment](https://en.wikipedia.org/wiki/Commitment_scheme) to the post-transaction state to L1.
  Note that `op-proposer` does not need to write a commitment after each transaction to L1; it is OK to commit to the state after a block of transactions.

<img src="https://mintcdn.com/optimism-373f39ad/Vw5vY8zA-CRjBsUF/public/img/op-stack/protocol/transaction-flow-dark-mode.svg?fit=max&auto=format&n=Vw5vY8zA-CRjBsUF&q=85&s=2b33db158e071269f183d3fa81c7cfb2" alt="Overall process." width="1265" height="685" data-path="public/img/op-stack/protocol/transaction-flow-dark-mode.svg" />

## Writing the transaction to L1

[`op-batcher`](https://github.com/ethereum-optimism/optimism/tree/develop/op-batcher) has two main jobs:

* Compress transactions into batches.
* Post those batches to L1 to ensure availability and integrity.

### Compression

The batcher aggregates [sequencer batches](https://specs.optimism.io/glossary.html?utm_source=op-docs\&utm_medium=docs#sequencer-batch) into [channels](https://specs.optimism.io/glossary.html?utm_source=op-docs\&utm_medium=docs#channel).
This allows for more data per compression frame, and therefore a better compression ratio.
You can read more about this process [in the specs](https://specs.optimism.io/protocol/derivation.html?utm_source=op-docs\&utm_medium=docs#batch-submission).

When a channel is full or times out it is compressed and written.

The maximum time that a channel can be open, from the first transaction to the last, is specified in units of L1 block time (so a value of 5 means 5\*12=60 seconds).
You can specify it either as an environment variable (`OP_BATCHER_MAX_CHANNEL_DURATION`) or a command line parameter (`--max-channel-duration`).
Alternatively, you can set it to zero (the default) to avoid posting smaller, less cost efficient transactions.

A channel is full when the anticipated compressed size is the target L1 transaction size.
This is controlled by two parameters:

1. The target L1 transaction size, which you can specify in bytes on the command line (`--target-l1-tx-size-bytes`) or as an environment variable (`OP_BATCHER_TARGET_L1_TX_SIZE_BYTES`)

2. The expected compression ratio, which you can specify as a decimal value, again either on the command line (`--approx-compr-ratio`) or as an environment variable (`OP_BATCHER_APPROX_COMPR_RATIO`).

You can see the code that implements this process in [`channel_manager.go`](https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/batcher/channel_manager.go) and [`channel_builder.go`](https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/batcher/channel_builder.go).

### Posting to L1

When a channel is full it is posted, either as a single transaction or as multiple transactions (depending on data size) to L1.

Writing to L1 is what moves a processed L2 transaction through the protocol's finality states: **unsafe** (processed, but not written to L1 yet), **safe** (written to L1), and **finalized** (the L1 block containing it has been finalized by Ethereum).
For what these states mean and when you can rely on a transaction being irreversible, see [Transaction finality](/op-stack/transactions/transaction-finality).

You can see the code that builds the channels to be written to L1 in [`channel_out.go`](https://github.com/ethereum-optimism/optimism/blob/develop/op-node/rollup/derive/channel_out.go) and [`channel_builder.go`](https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/batcher/channel_builder.go).
The transactions themselves are sent in [`op-batcher`'s main loop](https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/batcher/driver.go#L489-L520), which calls [`publishStateToL1`](https://github.com/ethereum-optimism/optimism/blob/develop/op-batcher/batcher/driver.go#L784)

#### Determining the status of a transaction

This is the procedure to see a transaction's status.
The directions here are for [Foundry](https://book.getfoundry.sh/), but the concept is the same regardless of the method you use.

1. Get the number of the L2 block in which the transaction is recorded.

   ```sh theme={null}
   export ETH_RPC_URL=<URL to Optimism network>
   cast tx <transaction hash> blockNumber
   ```

2. Get the number of the latest finalized block.
   If the result is greater than or equal to the block number of the transaction, the transaction is finalized.

   ```sh theme={null}
   cast block finalized --field number
   ```

3. Get the number of the latest safe block.
   If the result is greater than or equal to the block number of the transaction, the transaction is safe.

   ```sh theme={null}
   cast block safe --field number
   ```

4. If the transaction isn't finalized or safe, it's unsafe.

## State processing

State processing can be divided into two steps:

1. Applying the transaction to the old state to produce the new state, which is performed by the execution client (`op-reth` by default).

2. Proposing the new [Merkle root](https://en.wikipedia.org/wiki/Merkle_tree) of the state.
   Merkle roots are used because the actual state is long and would cost too much to write to L1.
   This step is performed by `op-proposer`.

### State changes

The state is stored and modified by the execution client. The default is [`op-reth`](https://github.com/paradigmxyz/reth).

<Warning>
  **op-geth has reached end-of-support (2026-05-31) and does not support the now-active Karst hardfork, so op-geth nodes can no longer follow the canonical chain.** Migrate to op-reth, the primary supported execution client. See the [op-geth deprecation notice](/notices/op-geth-deprecation) for the full migration plan.
</Warning>

### State root proposals

The state root proposals are posted by [`op-proposer`](https://github.com/ethereum-optimism/optimism/tree/develop/op-proposer) to [`L2OutputOracle`](https://github.com/ethereum-optimism/optimism/blob/233ede59d16cb01bdd8e7ff662a153a4c3178bdd/packages/contracts-bedrock/contracts/L1/L2OutputOracle.sol) (as of `233ede5`) on L1.

Output proposals are *not* immediately valid.
They can only be considered authoritative once the fault challenge period has passed; see [Withdrawal flow](/op-stack/bridging/withdrawal-flow) for how output roots are used to prove and finalize withdrawals.
