Repository: livepeer/go-livepeer (go)

Issue:
Track last seen L1 block in addition to last seen L2 block
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

There are a few places where `LastSeenBlock()` is either called either on the [TimeWatcher](https://github.com/livepeer/go-livepeer/blob/970d0aa7892cd9c30aaf069a7ee8e72d8fa64111/eth/watchers/timewatcher.go#L140) or the [DB](https://github.com/livepeer/go-livepeer/blob/970d0aa7892cd9c30aaf069a7ee8e72d8fa64111/common/db.go#L440). In the former case, the value returned is read from memory. In the latter case, the value returned is read from a sqlite3 DB [1]. When the node is connected to Arbitrum, in both cases, the method will return the *L2 block number*. This is because the node receives the latest block number using the [blockwatch package](https://github.com/livepeer/go-livepeer/blob/master/eth/blockwatch/block_watcher.go) which will poll for new block headers from the RPC provider. When connected to an Arbitrum RPC provider, the block headers will be for the Arbitrum L2 chain so the number included in each header will be the L2 block number. While it makes sense for the node to be monitoring L2 blocks for events that it cares about (since the events would be emitted from transactions included in those L2 blocks), the use of L2 block numbers in `LastSeenBlock()` can be problematic for a few reasons:

1. Os use the last seen block to set expiration params for tickets

- [The expiration block](https://github.com/livepeer/go-livepeer/blob/7836d1ce43ee4aabb2fc71ac29572018aceec564/pm/recipient.go#L200) for ticket params is set based on the last seen block
- During ticket validation, the expiration block is compared against [the last seen block](https://github.com/livepeer/go-livepeer/blob/7836d1ce43ee4aabb2fc71ac29572018aceec564/pm/recipient.go#L170)
- Bs will check the expiration block of ticket params against [the last seen block](https://github.com/livepeer/go-livepeer/blob/c218058f8d03200c8ad19f196bdacffae5cd09f6/pm/sender.go#L157) to determine whether they need to refresh a set of ticket params

Using a L2 block number can be problematic because ticket params expire after [a fixed # of blocks](https://github.com/livepeer/go-livepeer/blob/7836d1ce43ee4aabb2fc71ac29572018aceec564/pm/recipient.go#L24) (this value should probably be higher as well) which was originally set based on average L1 block times of ~13s. The current 10 blocks would be roughly (10 * 13) ~= 2m. L2 blocks are created much faster and can also be created in bursts so at the moment they do not seem as a reliable "clock" that nodes can coordinate around.

2. The round initializer service uses the last seen block to determine whether to submit a round initialization tx

- The [last seen block](https://github.com/livepeer/go-livepeer/blob/970d0aa7892cd9c30aaf069a7ee8e72d8fa64111/eth/roundinitializer.go#L100) is used to calculate a seed value to determine if the address for the node is responsible for submitting the tx

Using a L2 block number can be problematic because the start and end of rounds are still set in terms of L1 block numbers. So, in order for the round initialize to determine where we are in the length of a round a L2 block number wouldn't be helpful.

Note: With all this being said, there *are* cases where using the L2 block number from `LastSeenBlock()` is useful. The main case I see right now is to [get the last L2 block that the node has seen to determine which block headers that it should fetch on startup](https://github.com/livepeer/go-livepeer/blob/6c575bcc14fc48727ed48d9f7ec74edfc87d18e8/cmd/livepeer/livepeer.go#L469).

[1] The original motivation for having an in-memory *and* an on-disk cache was to read from the former when the value is needed on a latency sensitive code path and to read from the latter when not on a latency sensitive code path.

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

Since it seems useful for the node to be aware of its last seen L1 block number *and* last seen L2 block number, we could consider adding a `LastSeenL1Block()` method to complement the existing `LastSeenBlock()` method. I think we just need to implement this on `TimeWatcher` as that impl is used for the above cases, but I would double check if it should be implemented on `DB` as well in case the method on `DB` would be useful anywhere.

`LastSeenBlock()` should continue returning the last seen block number from the connected RPC provider.

`LastSeenL1Block()` should return the last seen L1 block number. If the node is connected to a L1 RPC provider this will be the same value as `LastSeenBlock()`. If the node is connected to a L2 RPC provider this will be a different value from `LastSeenBlock()`.

How will the node get the L1 block number if it is connected to a L2 RPC provider? Luckily, it looks like Arbitrum L2 RPC providers will include the L1 block number in a `l1BlockNumber` field in the response for the `eth_getBlockByNumber` RPC call. Use this command to confirm:

```
curl -X POST --data '{
    "id": 1337,
    "jsonrpc": "2.0",
    "method": "eth_getBlockByNumber",
    "params":["latest", true]
}' https://rinkeby.arbitrum.io/rpc | jq .
```

We could update the `blockwatch` package to also parse this `l1BlockNumber` field when fetching block headers.

The blockwatcher currently uses `blockwatch.Client` to send RPC requests to the RPC provider for block headers. Luckily, the [HeaderByNumber](https://github.com/livepeer/go-livepeer/blob/c6563c780f4ce09f0fa76db6f6d0cca636653eb1/eth/blockwatch/client.go#L55) method uses a raw RPC call for `eth_getBlockByNumber`. A good place to start could be to update the [struct](https://github.com/livepeer/go-livepeer/blob/c6563c780f4ce09f0fa76db6f6d0cca636653eb1/eth/blockwatch/client.go#L47) that the response is unmarshalled into to parse `l1BlockNumber` and store it as a field in the struct. Then, we'd have access to this value in the rest of the package and we can access it elsewhere in the codebase as is needed i.e. storing in-memory in `TimeWatcher`.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

N/A

**Additional context**
Add any other context or screenshots about the feature request here.

N/A

Pull request description:
Use L1 block number for Ticket Parameters and Round Initialization
**What does this pull request do? Explain your changes. (required)**
Use L1 block numbers during Round Initialization and in Ticket Params.

Important comments:
- The L1 block numbers are not stored in DB and therefore need to be enriched with L1 Block Number [using Arbitrum RPC API](https://github.com/leszko/go-livepeer/blob/2dbae16237ff82706324975e96050e28aea884a3/eth/blockwatch/block_watcher.go#L625); an alternative approach would be to store L1 Block Numbers in DB:
  - it would mean fewer calls to Aribtrum RPC API
  - it would be a backward-incompatible change
  - I think enriching blocks on-the-fly is good enough
- `TimeWatcher` now has a function `LastSeenL1Block()`, but I found it does not need to have `LastSeenBlock()` anymore
- Having now both `blocks` and `l1Blocks` in the code can be super-confusing, I tried to rename all the necessary parts of the code, so that `block` always refers to L2 Block, however, there are still a places for which we may need an additional renaming in the future:
   - [TicketParams](https://github.com/leszko/go-livepeer/blob/7125e3c9778622f216441baa7b951cbd009943db/pm/ticket.go#L34)
   - [lp_rpc.proto](https://github.com/leszko/go-livepeer/blob/4fceaff0fce6bed65dae12428006408dc5064e15/net/lp_rpc.proto#L1)
   - Eth contracts or/and contract bindings
- There is one place where we could think about optimizing more, which is to try to fetch L1 Number while fetching logs in [this part](https://github.com/leszko/go-livepeer/blob/079f4ebe0801327fec6664cd5756100cf19e4d82/eth/blockwatch/block_watcher.go#L398)), but it doesn't seem to be an issue at the moment.

**Specific updates (required)**
<!--- List out all significant updates your code introduces -->
- Add L1 Block Number while fetching blocks by Block Watcher from ETH API
- Use L1 Block Number in Time Watcher (and propagate it to Ticket Sender and Recipient)
- Rename all `block` into `l1Block` when it refers to L1 Blocks
- Refactor Block Watcher Client (reuse ETH API code)

**How did you test each of these updates (required)**
<!-- A detailed description of how you tested your code changes. Include details of your testing environment, and the tests you ran to see how your change affects other areas of the code, etc. -->
Tests on Arbitrum:
1. Added debug logs to Round Initializer to check that it uses L1 block numbers
2. Add debug logs to ticket params to check that that sender and receiver use  L1 block numbers for ticket params

**Does this pull request close any open issues?**
<!-- Fixes # -->
fix #2220

**Checklist:**
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] Read the [contribution guide](./doc/contributing.md)
- [x] `make` runs successfully
- [x] All tests in `./test.sh` pass
- [ ] README and other documentation updated
- [x] [Pending changelog](./CHANGELOG_PENDING.md) updated


Patch:
diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md
index d3b7df1e..c01ee6e1 100644
--- a/CHANGELOG_PENDING.md
+++ b/CHANGELOG_PENDING.md
@@ -7,8 +7,8 @@
 ### Features ⚒
 
 #### General
-
 - \#2208 Improve Feed PubSub: execute subscribers' blocking operations in separate goroutines (@leszko)
+- \#2222 Use L1 block number for Ticket Parameters and Round Initialization (@leszko)
 
 #### Broadcaster
 
diff --git a/eth/blockwatch/block_watcher.go b/eth/blockwatch/block_watcher.go
index 6e23aad3..b89fc8d7 100644
--- a/eth/blockwatch/block_watcher.go
+++ b/eth/blockwatch/block_watcher.go
@@ -97,7 +97,7 @@ func (w *Watcher) BackfillEventsIfNeeded(ctx context.Context) error {
 		return err
 	}
 	if len(events) > 0 {
-		w.blockFeed.Send(events)
+		w.blockFeed.Send(w.enrichWithL1(events))
 	}
 	return nil
 }
@@ -137,7 +137,11 @@ func (w *Watcher) Subscribe(sink chan<- []*Event) event.Subscription {
 
 // GetLatestBlock returns the latest block processed
 func (w *Watcher) GetLatestBlock() (*MiniHeader, error) {
-	return w.stack.Peek()
+	h, err := w.stack.Peek()
+	if err != nil {
+		return nil, err
+	}
+	return w.enrichWithL1BlockNumber(h)
 }
 
 // InspectRetainedBlocks returns the blocks retained in-memory by the Watcher instance. It is not
@@ -202,7 +206,7 @@ func (w *Watcher) pollNextBlock() error {
 	// Even if an error occurred, we still want to emit the events gathered since we might have
 	// popped blocks off the Stack and they won't be re-added
 	if len(events) != 0 {
-		w.blockFeed.Send(events)
+		w.blockFeed.Send(w.enrichWithL1(events))
 	}
 	if err != nil {
 		return err
@@ -609,6 +613,38 @@ func (w *Watcher) filterLogsRecursively(from, to int, allLogs []types.Log) ([]ty
 	return allLogs, nil
 }
 
+// enrichWithL1 adds L1 block number to the event with the highest block number.
+//
+// Adding L1 block number to an event requires an RPC call and the only code using L1 block number is TimeWatcher that
+// is interested only in the highest L1 block number, not in all of them.
+func (w *Watcher) enrichWithL1(events []*Event) []*Event {
+	if len(events) == 0 {
+		return events
+	}
+
+	max := 0
+	for i, e := range events {
+		if events[max].BlockHeader.Number.Cmp(e.BlockHeader.Number) <= 0 {
+			max = i
+		}
+	}
+
+	block, err := w.enrichWithL1BlockNumber(events[max].BlockHeader)
+	if err != nil {
+		glog.Errorf("Cannot fetch L1 block number for block number %d err=%q", events[max].BlockHeader.Number, err)
+	}
+
+	events[max].BlockHeader = block
+	return events
+}
+
+func (w *Watcher) enrichWithL1BlockNumber(header *MiniHeader) (*MiniHeader, error) {
+	if header == nil || header.L1BlockNumber != nil {
+		return header, nil
+	}
+	return w.client.HeaderByHash(header.Hash)
+}
+
 func isUnknownBlockErr(err error) bool {
 	// Geth error
 	if err.Error() == "unknown block" {
diff --git a/eth/blockwatch/client.go b/eth/blockwatch/client.go
index 36d8569f..3065b18a 100644
--- a/eth/blockwatch/client.go
+++ b/eth/blockwatch/client.go
@@ -3,6 +3,7 @@ package blockwatch
 import (
 	"context"
 	"errors"
+	"fmt"
 	"math/big"
 	"time"
 
@@ -44,33 +45,39 @@ func NewRPCClient(rpcURL string, requestTimeout time.Duration) (*RPCClient, erro
 	return &RPCClient{rpcClient: rpcClient, client: ethClient, requestTimeout: requestTimeout}, nil
 }
 
-type getBlockByNumberResponse struct {
-	Hash       common.Hash `json:"hash"`
-	ParentHash common.Hash `json:"parentHash"`
-	Number     string      `json:"number"`
+type getHeaderResponse struct {
+	Hash          common.Hash `json:"hash"`
+	ParentHash    common.Hash `json:"parentHash"`
+	Number        string      `json:"number"`
+	L1BlockNumber string      `json:"l1BlockNumber"`
 }
 
 // HeaderByNumber fetches a block header by its number. If no `number` is supplied, it will return the latest
 // block header. If no block exists with this number it will return a `ethereum.NotFound` error.
 func (rc *RPCClient) HeaderByNumber(number *big.Int) (*MiniHeader, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), rc.requestTimeout)
-	defer cancel()
-
 	var blockParam string
 	if number == nil {
 		blockParam = "latest"
 	} else {
 		blockParam = hexutil.EncodeBig(number)
 	}
-	shouldIncludeTransactions := false
 
-	// Note(fabio): We use a raw RPC call here instead of `EthClient`'s `BlockByNumber()` method because block
-	// hashes are computed differently on Kovan vs. mainnet, resulting in the wrong block hash being returned by
-	// `BlockByNumber` when using Kovan. By doing a raw RPC call, we can simply use the blockHash returned in the
-	// RPC response rather than re-compute it from the block header.
-	// Source: https://github.com/ethereum/go-ethereum/pull/18166
-	var header getBlockByNumberResponse
-	err := rc.rpcClient.CallContext(ctx, &header, "eth_getBlockByNumber", blockParam, shouldIncludeTransactions)
+	return rc.callEth("eth_getBlockByNumber", blockParam)
+}
+
+// HeaderByHash fetches a block header by its block hash. If no block exists with this number it will return
+// a `ethereum.NotFound` error.
+func (rc *RPCClient) HeaderByHash(hash common.Hash) (*MiniHeader, error) {
+	return rc.callEth("eth_getBlockByHash", hash)
+}
+
+func (rc *RPCClient) callEth(method string, arg interface{}) (*MiniHeader, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), rc.requestTimeout)
+	defer cancel()
+
+	var header getHeaderResponse
+	err := rc.rpcClient.CallContext(ctx, &header, method, arg, false)
+
 	if err != nil {
 		return nil, err
 	}
@@ -79,31 +86,24 @@ func (rc *RPCClient) HeaderByNumber(number *big.Int) (*MiniHeader, error) {
 		return nil, ethereum.NotFound
 	}
 
+	// If no L1BlockNumber, then Livepeer is running on L1, so L1BlockNumber is the same as BlockNumber
+	if header.L1BlockNumber == "" {
+		header.L1BlockNumber = header.Number
+	}
+
 	blockNum, ok := math.ParseBig256(header.Number)
 	if !ok {
 		return nil, errors.New("Failed to parse big.Int value from hex-encoded block number returned from eth_getBlockByNumber")
 	}
-	miniHeader := &MiniHeader{
-		Hash:   header.Hash,
-		Parent: header.ParentHash,
-		Number: blockNum,
-	}
-	return miniHeader, nil
-}
-
-// HeaderByHash fetches a block header by its block hash. If no block exists with this number it will return
-// a `ethereum.NotFound` error.
-func (rc *RPCClient) HeaderByHash(hash common.Hash) (*MiniHeader, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), rc.requestTimeout)
-	defer cancel()
-	header, err := rc.client.HeaderByHash(ctx, hash)
-	if err != nil {
-		return nil, err
+	l1BlockNum, ok := math.ParseBig256(header.L1BlockNumber)
+	if !ok {
+		return nil, fmt.Errorf("Failed to parse big.Int value from hex-encoded L1 block number returned from %v", method)
 	}
 	miniHeader := &MiniHeader{
-		Hash:   header.Hash(),
-		Parent: header.ParentHash,
-		Number: header.Number,
+		Hash:          header.Hash,
+		Parent:        header.ParentHash,
+		Number:        blockNum,
+		L1BlockNumber: l1BlockNum,
 	}
 	return miniHeader, nil
 }
diff --git a/eth/blockwatch/fake_client.go b/eth/blockwatch/fake_client.go
index 42cf48c4..05bddba0 100644
--- a/eth/blockwatch/fake_client.go
+++ b/eth/blockwatch/fake_client.go
@@ -12,6 +12,12 @@ import (
 	"github.com/ethereum/go-ethereum/core/types"
 )
 
+const (
+	FakeHash          = "0x6bbf9b6e836207ab25379c20e517a89090cbbaf8877746f6ed7fb6820770816b"
+	FakeBlockNumber   = 30
+	FakeL1BlockNumber = 50
+)
+
 // fixtureTimestep holds the JSON-RPC data available at every timestep of the simulation.
 type fixtureTimestep struct {
 	GetLatestBlock   MiniHeader                 `json:"getLatestBlock"  gencodec:"required"`
@@ -65,6 +71,15 @@ func (fc *fakeClient) HeaderByNumber(number *big.Int) (*MiniHeader, error) {
 // HeaderByHash fetches a block header by its block hash. If no block exists with this number it will return
 // a `ethereum.NotFound` error.
 func (fc *fakeClient) HeaderByHash(hash common.Hash) (*MiniHeader, error) {
+	// predefined block
+	if hash.String() == FakeHash {
+		return &MiniHeader{
+			Hash:          common.HexToHash(FakeHash),
+			Number:        big.NewInt(FakeBlockNumber),
+			L1BlockNumber: big.NewInt(FakeL1BlockNumber),
+		}, nil
+	}
+
 	fc.fixtureMut.Lock()
 	defer fc.fixtureMut.Unlock()
 	timestep := fc.fixtureData[fc.currentTimestep]
@@ -87,10 +102,10 @@ func (fc *fakeClient) FilterLogs(q ethereum.FilterQuery) ([]types.Log, error) {
 				common.HexToHash("0x0000000000000000000000004bdd0d16cfa18e33860470fc4d65c6f5cee60959"),
 			},
 			Data:        common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000337ad34c0"),
-			BlockNumber: uint64(30),
+			BlockNumber: uint64(FakeBlockNumber),
 			TxHash:      common.HexToHash("0xd9bb5f9e888ee6f74bedcda811c2461230f247c205849d6f83cb6c3925e54586"),
 			TxIndex:     uint(0),
-			BlockHash:   common.HexToHash("0x6bbf9b6e836207ab25379c20e517a89090cbbaf8877746f6ed7fb6820770816b"),
+			BlockHash:   common.HexToHash(FakeHash),
 			Index:       uint(0),
 			Removed:     false,
 		},
diff --git a/eth/blockwatch/stack.go b/eth/blockwatch/stack.go
index 28b4d462..c100fd86 100644
--- a/eth/blockwatch/stack.go
+++ b/eth/blockwatch/stack.go
@@ -10,10 +10,11 @@ import (
 
 // MiniHeader is a succinct representation of an Ethereum block header
 type MiniHeader struct {
-	Hash   ethcommon.Hash
-	Parent ethcommon.Hash
-	Number *big.Int
-	Logs   []types.Log
+	Hash          ethcommon.Hash
+	Parent        ethcommon.Hash
+	Number        *big.Int
+	L1BlockNumber *big.Int
+	Logs          []types.Log
 }
 
 // MiniHeaderStore is an interface for a store that manages the state of a MiniHeader collection
diff --git a/eth/roundinitializer.go b/eth/roundinitializer.go
index 5ae62fd9..48da2dc0 100644
--- a/eth/roundinitializer.go
+++ b/eth/roundinitializer.go
@@ -10,22 +10,22 @@ import (
 	"github.com/golang/glog"
 )
 
-// Number of blocks in an epoch which is the time period during which the caller should
+// Number of L1 blocks in an epoch which is the time period during which the caller should
 // initialize the round if it is selected and if the round is not initialized
-var epochBlocks = big.NewInt(5)
+var epochL1Blocks = big.NewInt(5)
 
 type timeWatcher interface {
-	LastSeenBlock() *big.Int
+	LastSeenL1Block() *big.Int
 	LastInitializedRound() *big.Int
-	LastInitializedBlockHash() [32]byte
-	CurrentRoundStartBlock() *big.Int
+	LastInitializedL1BlockHash() [32]byte
+	CurrentRoundStartL1Block() *big.Int
 	SubscribeRounds(sink chan<- types.Log) event.Subscription
-	SubscribeBlocks(sink chan<- *big.Int) event.Subscription
+	SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription
 	GetTranscoderPoolSize() *big.Int
 }
 
 // RoundInitializer is a service that automatically initializes the current round. Each round is split into epochs with a length of
-// epochBlocks. During each epoch a member of the upcoming active set is selected to initialize the round
+// epochL1Blocks. During each epoch a member of the upcoming active set is selected to initialize the round
 // This selection process is purely a client side implementation that attempts to minimize on-chain transaction collisions, but
 // collisions are still possible if initialization transactions are submitted by parties that are not using this selection process
 type RoundInitializer struct {
@@ -33,7 +33,7 @@ type RoundInitializer struct {
 	tw     timeWatcher
 	quit   chan struct{}
 
-	nextRoundStartBlock *big.Int
+	nextRoundStartL1Block *big.Int
 	mu                  sync.Mutex
 }
 
@@ -48,9 +48,9 @@ func NewRoundInitializer(client LivepeerEthClient, tw timeWatcher) *RoundInitial
 
 // Start kicks off a loop that checks if the round should be initialized
 func (r *RoundInitializer) Start() error {
-	blockSink := make(chan *big.Int, 10)
-	blockSub := r.tw.SubscribeBlocks(blockSink)
-	defer blockSub.Unsubscribe()
+	l1BlockSink := make(chan *big.Int, 10)
+	l1BlockSub := r.tw.SubscribeL1Blocks(l1BlockSink)
+	defer l1BlockSub.Unsubscribe()
 
 	roundSink := make(chan types.Log, 10)
 	roundSub := r.tw.SubscribeRounds(roundSink)
@@ -61,30 +61,30 @@ func (r *RoundInitializer) Start() error {
 		return err
 	}
 
-	currentRoundStartBlock, err := r.client.CurrentRoundStartBlock()
+	currentRoundStartL1Block, err := r.client.CurrentRoundStartBlock()
 	if err != nil {
 		return err
 	}
 
-	r.nextRoundStartBlock = new(big.Int).Add(currentRoundStartBlock, roundLength)
+	r.nextRoundStartL1Block = new(big.Int).Add(currentRoundStartL1Block, roundLength)
 
 	for {
 		select {
 		case <-r.quit:
 			glog.Infof("Stopping round initializer")
 			return nil
-		case err := <-blockSub.Err():
+		case err := <-l1BlockSub.Err():
 			if err != nil {
-				glog.Errorf("Block subscription error err=%q", err)
+				glog.Errorf("L1 Block subscription error err=%q", err)
 			}
 		case err := <-roundSub.Err():
 			if err != nil {
 				glog.Errorf("Round subscription error err=%q", err)
 			}
 		case <-roundSink:
-			r.nextRoundStartBlock = r.nextRoundStartBlock.Add(r.tw.CurrentRoundStartBlock(), roundLength)
-		case block := <-blockSink:
-			if block.Cmp(r.nextRoundStartBlock) >= 0 {
+			r.nextRoundStartL1Block = r.nextRoundStartL1Block.Add(r.tw.CurrentRoundStartL1Block(), roundLength)
+		case l1Block := <-l1BlockSink:
+			if l1Block.Cmp(r.nextRoundStartL1Block) >= 0 {
 				go func() {
 					if err := r.tryInitialize(); err != nil {
 						glog.Error(err)
@@ -104,10 +104,10 @@ func (r *RoundInitializer) tryInitialize() error {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
-	currentBlk := r.tw.LastSeenBlock()
-	lastInitializedBlkHash := r.tw.LastInitializedBlockHash()
+	currentL1Blk := r.tw.LastSeenL1Block()
+	lastInitializedL1BlkHash := r.tw.LastInitializedL1BlockHash()
 
-	epochSeed := r.currentEpochSeed(currentBlk, r.nextRoundStartBlock, lastInitializedBlkHash)
+	epochSeed := r.currentEpochSeed(currentL1Blk, r.nextRoundStartL1Block, lastInitializedL1BlkHash)
 
 	ok, err := r.shouldInitialize(epochSeed)
 	if err != nil {
@@ -180,11 +180,11 @@ func (r *RoundInitializer) shouldInitialize(epochSeed *big.Int) (bool, error) {
 // Returns the seed used to select a round initializer in the current epoch for the current round
 // This seed is not meant to be unpredictable. The only requirement for the seed is that it is calculated the same way for each
 // party running the round initializer
-func (r *RoundInitializer) currentEpochSeed(currentBlock, roundStartBlock *big.Int, lastInitializedBlkHash [32]byte) *big.Int {
-	epochNum := new(big.Int).Sub(currentBlock, roundStartBlock)
-	epochNum.Div(epochNum, epochBlocks)
+func (r *RoundInitializer) currentEpochSeed(currentL1Block, roundStartL1Block *big.Int, lastInitializedL1BlkHash [32]byte) *big.Int {
+	epochNum := new(big.Int).Sub(currentL1Block, roundStartL1Block)
+	epochNum.Div(epochNum, epochL1Blocks)
 
 	// The seed for the current epoch is calculated as:
-	// keccak256(lastInitializedBlkHash | epochNum)
-	return crypto.Keccak256Hash(append(lastInitializedBlkHash[:], epochNum.Bytes()...)).Big()
+	// keccak256(lastInitializedL1BlkHash | epochNum)
+	return crypto.Keccak256Hash(append(lastInitializedL1BlkHash[:], epochNum.Bytes()...)).Big()
 }
diff --git a/eth/watchers/stub.go b/eth/watchers/stub.go
index afdff731..767428c6 100644
--- a/eth/watchers/stub.go
+++ b/eth/watchers/stub.go
@@ -342,9 +342,10 @@ func (s *stubUnbondingLockStore) Get(id int64) *stubUnbondingLock {
 
 func defaultMiniHeader() *blockwatch.MiniHeader {
 	block := &blockwatch.MiniHeader{
-		Number: big.NewInt(450),
-		Parent: pm.RandHash(),
-		Hash:   pm.RandHash(),
+		Number:        big.NewInt(450),
+		L1BlockNumber: big.NewInt(650),
+		Parent:        pm.RandHash(),
+		Hash:          pm.RandHash(),
 	}
 	log := types.Log{
 		Topics:    []ethcommon.Hash{pm.RandHash(), pm.RandHash()},
diff --git a/eth/watchers/timewatcher.go b/eth/watchers/timewatcher.go
index f79e686a..28f8322c 100644
--- a/eth/watchers/timewatcher.go
+++ b/eth/watchers/timewatcher.go
@@ -26,20 +26,20 @@ import (
 //	* Last Seen Block Number
 type TimeWatcher struct {
 	// state
-	mu                       sync.RWMutex
-	lastInitializedRound     *big.Int
-	lastInitializedBlockHash [32]byte
-	currentRoundStartBlock   *big.Int
-	transcoderPoolSize       *big.Int
-	lastSeenBlock            *big.Int
+	mu                         sync.RWMutex
+	lastInitializedRound       *big.Int
+	lastInitializedL1BlockHash [32]byte
+	currentRoundStartL1Block   *big.Int
+	transcoderPoolSize         *big.Int
+	lastSeenL1Block            *big.Int
 
 	// last initialized round number subscription feeds
 	roundSubFeed  event.Feed
 	roundSubScope event.SubscriptionScope
 	// last seen block number subscription feeds
-	blockSubFeed  event.Feed
-	blockSubScope event.SubscriptionScope
-	feedMu        sync.Mutex
+	l1BlockSubFeed  event.Feed
+	l1BlockSubScope event.SubscriptionScope
+	feedMu          sync.Mutex
 
 	watcher BlockWatcher
 	lpEth   eth.LivepeerEthClient
@@ -68,7 +68,7 @@ func NewTimeWatcher(roundsManagerAddr ethcommon.Address, watcher BlockWatcher, l
 	}
 	bh, err := tw.lpEth.BlockHashForRound(lr)
 	if err != nil {
-		return nil, fmt.Errorf("error fetching initial lastInitializedBlockHash value err=%q", err)
+		return nil, fmt.Errorf("error fetching initial lastInitializedL1BlockHash value err=%q", err)
 	}
 	num, err := tw.lpEth.CurrentRoundStartBlock()
 	if err != nil {
@@ -80,11 +80,11 @@ func NewTimeWatcher(roundsManagerAddr ethcommon.Address, watcher BlockWatcher, l
 	if err != nil {
 		return nil, fmt.Errorf("error fetching last seen block err=%q", err)
 	}
-	blockNum := big.NewInt(0)
+	l1BlockNum := big.NewInt(0)
 	if lastSeenBlock != nil {
-		blockNum = lastSeenBlock.Number
+		l1BlockNum = lastSeenBlock.L1BlockNumber
 	}
-	tw.setLastSeenBlock(blockNum)
+	tw.setLastSeenL1Block(l1BlockNum)
 
 	size, err := tw.lpEth.GetTranscoderPoolSize()
 	if err != nil {
@@ -103,24 +103,24 @@ func (tw *TimeWatcher) LastInitializedRound() *big.Int {
 }
 
 // LastInitializedBlockHash returns the blockhash of the block the last round was initiated in
-func (tw *TimeWatcher) LastInitializedBlockHash() [32]byte {
+func (tw *TimeWatcher) LastInitializedL1BlockHash() [32]byte {
 	tw.mu.RLock()
 	defer tw.mu.RUnlock()
-	return tw.lastInitializedBlockHash
+	return tw.lastInitializedL1BlockHash
 }
 
-func (tw *TimeWatcher) CurrentRoundStartBlock() *big.Int {
+func (tw *TimeWatcher) CurrentRoundStartL1Block() *big.Int {
 	tw.mu.RLock()
 	defer tw.mu.RUnlock()
-	return tw.currentRoundStartBlock
+	return tw.currentRoundStartL1Block
 }
 
 func (tw *TimeWatcher) setLastInitializedRound(round *big.Int, hash [32]byte, startBlk *big.Int) {
 	tw.mu.Lock()
 	defer tw.mu.Unlock()
 	tw.lastInitializedRound = round
-	tw.lastInitializedBlockHash = hash
-	tw.currentRoundStartBlock = startBlk
+	tw.lastInitializedL1BlockHash = hash
+	tw.currentRoundStartL1Block = startBlk
 }
 
 func (tw *TimeWatcher) GetTranscoderPoolSize() *big.Int {
@@ -138,16 +138,16 @@ func (tw *TimeWatcher) setTranscoderPoolSize(size *big.Int) {
 	tw.transcoderPoolSize = size
 }
 
-func (tw *TimeWatcher) LastSeenBlock() *big.Int {
+func (tw *TimeWatcher) LastSeenL1Block() *big.Int {
 	tw.mu.RLock()
 	defer tw.mu.RUnlock()
-	return tw.lastSeenBlock
+	return tw.lastSeenL1Block
 }
 
-func (tw *TimeWatcher) setLastSeenBlock(blockNum *big.Int) {
+func (tw *TimeWatcher) setLastSeenL1Block(blockNum *big.Int) {
 	tw.mu.Lock()
 	defer tw.mu.Unlock()
-	tw.lastSeenBlock = blockNum
+	tw.lastSeenL1Block = blockNum
 }
 
 // Watch the blockwatch subscription for NewRound events
@@ -175,24 +175,24 @@ func (tw *TimeWatcher) SubscribeRounds(sink chan<- types.Log) event.Subscription
 	return tw.roundSubScope.Track(tw.roundSubFeed.Subscribe(sink))
 }
 
-// SubscribeBlocks allows one to subscribe to newly seen block numbers
+// SubscribeL1Blocks allows one to subscribe to newly seen L1 block numbers
 // To unsubscribe, simply call `Unsubscribe` on the returned subscription.
 // The sink channel should have ample buffer space to avoid blocking other subscribers.
 // Slow subscribers are not dropped.
-func (tw *TimeWatcher) SubscribeBlocks(sink chan<- *big.Int) event.Subscription {
-	return tw.blockSubScope.Track(tw.blockSubFeed.Subscribe(sink))
+func (tw *TimeWatcher) SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription {
+	return tw.l1BlockSubScope.Track(tw.l1BlockSubFeed.Subscribe(sink))
 }
 
 // Stop TimeWatcher
 func (tw *TimeWatcher) Stop() {
 	close(tw.quit)
-	tw.blockSubScope.Close()
+	tw.l1BlockSubScope.Close()
 	tw.roundSubScope.Close()
 }
 
 func (tw *TimeWatcher) handleBlockEvents(events []*blockwatch.Event) {
 	for _, event := range events {
-		tw.handleBlockNum(event)
+		tw.handleL1BlockNum(event)
 		for _, log := range event.BlockHeader.Logs {
 			if event.Type == blockwatch.Removed {
 				log.Removed = true
@@ -204,15 +204,15 @@ func (tw *TimeWatcher) handleBlockEvents(events []*blockwatch.Event) {
 	}
 }
 
-func (tw *TimeWatcher) handleBlockNum(event *blockwatch.Event) {
+func (tw *TimeWatcher) handleL1BlockNum(event *blockwatch.Event) {
 	tw.feedMu.Lock()
 	defer tw.feedMu.Unlock()
 
-	last := tw.LastSeenBlock()
-	new := event.BlockHeader.Number
-	if last == nil || last.Cmp(new) != 0 {
-		tw.setLastSeenBlock(new)
-		tw.blockSubFeed.Send(new)
+	last := tw.LastSeenL1Block()
+	new := event.BlockHeader.L1BlockNumber
+	if new != nil && (last == nil || last.Cmp(new) != 0) {
+		tw.setLastSeenL1Block(new)
+		tw.l1BlockSubFeed.Send(new)
 	}
 }
 
@@ -235,7 +235,7 @@ func (tw *TimeWatcher) handleLog(log types.Log) error {
 		return fmt.Errorf("unable to decode event: %v", err)
 	}
 
-	roundStartBlock, err := tw.lpEth.CurrentRoundStartBlock()
+	roundStartL1Block, err := tw.lpEth.CurrentRoundStartBlock()
 	if err != nil {
 		return err
 	}
@@ -248,9 +248,9 @@ func (tw *TimeWatcher) handleLog(log types.Log) error {
 		if err != nil {
 			return err
 		}
-		tw.setLastInitializedRound(lr, bh, roundStartBlock)
+		tw.setLastInitializedRound(lr, bh, roundStartL1Block)
 	} else {
-		tw.setLastInitializedRound(nr.Round, nr.BlockHash, roundStartBlock)
+		tw.setLastInitializedRound(nr.Round, nr.BlockHash, roundStartL1Block)
 	}
 
 	// Get the active transcoder pool size when we receive a NewRound event
diff --git a/pm/broker.go b/pm/broker.go
index a1e6e841..7afa405a 100644
--- a/pm/broker.go
+++ b/pm/broker.go
@@ -71,15 +71,15 @@ type TimeManager interface {
 	// LastInitializedRound returns the last initialized round of the Livepeer protocol
 	LastInitializedRound() *big.Int
 	// LastInitializedBlockHash returns the blockhash of the block the last round was initiated in
-	LastInitializedBlockHash() [32]byte
+	LastInitializedL1BlockHash() [32]byte
 	// GetTranscoderPoolSize returns the size of the active transcoder set for a round
 	GetTranscoderPoolSize() *big.Int
 	// LastSeenBlock returns the last seen block number
-	LastSeenBlock() *big.Int
+	LastSeenL1Block() *big.Int
 	// SubscribeRounds allows one to subscribe to new round events
 	SubscribeRounds(sink chan<- types.Log) event.Subscription
 	// SubscribeBlocks allows one to subscribe to newly seen block numbers
-	SubscribeBlocks(sink chan<- *big.Int) event.Subscription
+	SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription
 }
 
 // SenderManager defines the methods for fetching sender information
diff --git a/pm/queue.go b/pm/queue.go
index 60a01251..a969c09c 100644
--- a/pm/queue.go
+++ b/pm/queue.go
@@ -94,17 +94,17 @@ func (q *ticketQueue) Length() (int, error) {
 // is sufficient to cover the face value of the ticket at the head of the queue. If the max float is sufficient, we pop
 // the ticket at the head of the queue and send it into q.redeemable which an external listener can use to receive redeemable tickets
 func (q *ticketQueue) startQueueLoop() {
-	blockSink := make(chan *big.Int, 10)
-	sub := q.tm.SubscribeBlocks(blockSink)
+	l1BlockSink := make(chan *big.Int, 10)
+	sub := q.tm.SubscribeL1Blocks(l1BlockSink)
 	defer sub.Unsubscribe()
 
 	for {
 		select {
 		case err := <-sub.Err():
 			if err != nil {
-				glog.Errorf("Block subscription error err=%q", err)
+				glog.Errorf("L1 Block subscription error err=%q", err)
 			}
-		case block := <-blockSink:
+		case block := <-l1BlockSink:
 			go q.handleBlockEvent(block)
 		case <-q.quit:
 			return
@@ -112,7 +112,7 @@ func (q *ticketQueue) startQueueLoop() {
 	}
 }
 
-func (q *ticketQueue) handleBlockEvent(block *big.Int) {
+func (q *ticketQueue) handleBlockEvent(latestL1Block *big.Int) {
 	q.mu.Lock()
 	defer q.mu.Unlock()
 
@@ -131,7 +131,7 @@ func (q *ticketQueue) handleBlockEvent(block *big.Int) {
 			return
 		}
 
-		if nextTicket.ParamsExpirationBlock.Cmp(block) <= 0 {
+		if nextTicket.ParamsExpirationBlock.Cmp(latestL1Block) <= 0 {
 			resCh := make(chan struct {
 				txHash ethcommon.Hash
 				err    error
diff --git a/pm/recipient.go b/pm/recipient.go
index ed6d8cd0..55241127 100644
--- a/pm/recipient.go
+++ b/pm/recipient.go
@@ -166,8 +166,8 @@ func (r *recipient) ReceiveTicket(ticket *Ticket, sig []byte, seed *big.Int) (st
 	}
 
 	// check advertised params aren't expired
-	latestBlock := r.tm.LastSeenBlock()
-	if ticket.ParamsExpirationBlock.Cmp(latestBlock) <= 0 {
+	latestL1Block := r.tm.LastSeenL1Block()
+	if ticket.ParamsExpirationBlock.Cmp(latestL1Block) <= 0 {
 		return sessionID, won, ErrTicketParamsExpired
 	}
 
@@ -196,17 +196,17 @@ func (r *recipient) TicketParams(sender ethcommon.Address, price *big.Rat) (*Tic
 		}
 	}
 
-	lastBlock := r.tm.LastSeenBlock()
-	expirationBlock := new(big.Int).Add(lastBlock, paramsExpirationBlock)
+	lastL1Block := r.tm.LastSeenL1Block()
+	expirationL1Block := new(big.Int).Add(lastL1Block, paramsExpirationBlock)
 
 	winProb := r.winProb(faceValue)
 
 	ticketExpirationParams := &TicketExpirationParams{
 		CreationRound:          r.tm.LastInitializedRound().Int64(),
-		CreationRoundBlockHash: r.tm.LastInitializedBlockHash(),
+		CreationRoundBlockHash: r.tm.LastInitializedL1BlockHash(),
 	}
 
-	recipientRand := r.rand(seed, sender, faceValue, winProb, expirationBlock, price, ticketExpirationParams)
+	recipientRand := r.rand(seed, sender, faceValue, winProb, expirationL1Block, price, ticketExpirationParams)
 	recipientRandHash := crypto.Keccak256Hash(ethcommon.LeftPadBytes(recipientRand.Bytes(), uint256Size))
 
 	return &TicketParams{
@@ -215,7 +215,7 @@ func (r *recipient) TicketParams(sender ethcommon.Address, price *big.Rat) (*Tic
 		WinProb:           winProb,
 		RecipientRandHash: recipientRandHash,
 		Seed:              seed,
-		ExpirationBlock:   expirationBlock,
+		ExpirationBlock:   expirationL1Block,
 		PricePerPixel:     price,
 		ExpirationParams:  ticketExpirationParams,
 	}, nil
@@ -356,7 +356,7 @@ func (r *recipient) EV() *big.Rat {
 
 func (r *recipient) senderNoncesCleanupLoop() {
 	sink := make(chan *big.Int, 10)
-	sub := r.tm.SubscribeBlocks(sink)
+	sub := r.tm.SubscribeL1Blocks(sink)
 	defer sub.Unsubscribe()
 	for {
 		select {
@@ -364,10 +364,10 @@ func (r *recipient) senderNoncesCleanupLoop() {
 			return
 		case err := <-sub.Err():
 			glog.Error(err)
-		case latestBlock := <-sink:
+		case latestL1Block := <-sink:
 			r.senderNoncesLock.Lock()
 			for recipientRand, sn := range r.senderNonces {
-				if sn.expirationBlock.Cmp(latestBlock) <= 0 {
+				if sn.expirationBlock.Cmp(latestL1Block) <= 0 {
 					delete(r.senderNonces, recipientRand)
 				}
 			}
diff --git a/pm/sender.go b/pm/sender.go
index 95de7475..b41524b6 100644
--- a/pm/sender.go
+++ b/pm/sender.go
@@ -154,9 +154,9 @@ func (s *sender) validateTicketParams(ticketParams *TicketParams, numTickets int
 		return nil
 	}
 
-	latestBlock := s.timeManager.LastSeenBlock()
+	latestL1Block := s.timeManager.LastSeenL1Block()
 
-	currentBuffer := new(big.Int).Sub(ticketParams.ExpirationBlock, latestBlock).Int64()
+	currentBuffer := new(big.Int).Sub(ticketParams.ExpirationBlock, latestL1Block).Int64()
 	if currentBuffer <= paramsExpiryBuffer {
 		return ErrTicketParamsExpired
 	}
@@ -194,7 +194,7 @@ func (s *sender) validateTicketParams(ticketParams *TicketParams, numTickets int
 
 func (s *sender) expirationParams() *TicketExpirationParams {
 	round := s.timeManager.LastInitializedRound()
-	blkHash := s.timeManager.LastInitializedBlockHash()
+	blkHash := s.timeManager.LastInitializedL1BlockHash()
 
 	return &TicketExpirationParams{
 		CreationRound:          round.Int64(),
diff --git a/pm/stub.go b/pm/stub.go
index 3d821141..c848dfa7 100644
--- a/pm/stub.go
+++ b/pm/stub.go
@@ -279,7 +279,7 @@ func (m *stubTimeManager) LastInitializedRound() *big.Int {
 	return m.round
 }
 
-func (m *stubTimeManager) LastInitializedBlockHash() [32]byte {
+func (m *stubTimeManager) LastInitializedL1BlockHash() [32]byte {
 	return m.blkHash
 }
 
@@ -287,7 +287,7 @@ func (m *stubTimeManager) GetTranscoderPoolSize() *big.Int {
 	return m.transcoderPoolSize
 }
 
-func (m *stubTimeManager) LastSeenBlock() *big.Int {
+func (m *stubTimeManager) LastSeenL1Block() *big.Int {
 	return m.lastSeenBlock
 }
 
@@ -297,7 +297,7 @@ func (m *stubTimeManager) SubscribeRounds(sink chan<- types.Log) event.Subscript
 	return m.roundSub
 }
 
-func (m *stubTimeManager) SubscribeBlocks(sink chan<- *big.Int) event.Subscription {
+func (m *stubTimeManager) SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription {
 	m.blockNumSink = sink
 	m.blockNumSub = &stubSubscription{errCh: make(<-chan error)}
 	return m.blockNumSub


Test patch:
diff --git a/eth/blockwatch/block_watcher_test.go b/eth/blockwatch/block_watcher_test.go
index 06e43112..3e4b88ee 100644
--- a/eth/blockwatch/block_watcher_test.go
+++ b/eth/blockwatch/block_watcher_test.go
@@ -551,3 +551,76 @@ func aRange(from, to int) string {
 	r := fmt.Sprintf("%d-%d", from, to)
 	return r
 }
+
+func TestEnrichWithL1_Empty(t *testing.T) {
+	assert := assert.New(t)
+
+	w := New(config)
+	res := w.enrichWithL1([]*Event{})
+	assert.NotNil(res)
+	assert.Empty(res)
+}
+
+func TestEnrichWithL1_One(t *testing.T) {
+	assert := assert.New(t)
+
+	config.Client = &fakeClient{}
+	w := New(config)
+	header := &MiniHeader{
+		Number: big.NewInt(FakeBlockNumber),
+		Hash:   common.HexToHash(FakeHash),
+	}
+	events := []*Event{{BlockHeader: header}}
+
+	res := w.enrichWithL1(events)
+
+	assert.Len(res, 1)
+	assert.Equal(common.HexToHash(FakeHash), res[0].BlockHeader.Hash)
+	assert.Equal(big.NewInt(FakeBlockNumber), res[0].BlockHeader.Number)
+	assert.Equal(big.NewInt(FakeL1BlockNumber), res[0].BlockHeader.L1BlockNumber)
+}
+
+func TestEnrichWithL1_Multiple(t *testing.T) {
+	assert := assert.New(t)
+
+	config.Client = &fakeClient{}
+	w := New(config)
+	highestHeader := &MiniHeader{
+		Number: big.NewInt(FakeBlockNumber),
+		Hash:   common.HexToHash(FakeHash),
+	}
+	events := []*Event{
+		{BlockHeader: &MiniHeader{Number: big.NewInt(1)}},
+		{BlockHeader: highestHeader},
+		{BlockHeader: &MiniHeader{Number: big.NewInt(2)}},
+	}
+
+	res := w.enrichWithL1(events)
+
+	assert.Len(res, 3)
+	// highest block number event has L1 block number
+	assert.Equal(common.HexToHash(FakeHash), res[1].BlockHeader.Hash)
+	assert.Equal(big.NewInt(FakeBlockNumber), res[1].BlockHeader.Number)
+	assert.Equal(big.NewInt(FakeL1BlockNumber), res[1].BlockHeader.L1BlockNumber)
+	// other events do not have L1 block numbers
+	assert.Nil(res[0].BlockHeader.L1BlockNumber)
+	assert.Nil(res[2].BlockHeader.L1BlockNumber)
+}
+
+func TestEnrichWithL1BlockNumber(t *testing.T) {
+	assert := assert.New(t)
+
+	config.Client = &fakeClient{}
+	w := New(config)
+	header := &MiniHeader{
+		Number: big.NewInt(FakeBlockNumber),
+		Hash:   common.HexToHash(FakeHash),
+	}
+
+	res, err := w.enrichWithL1BlockNumber(header)
+
+	assert.NoError(err)
+	assert.Equal(common.HexToHash(FakeHash), res.Hash)
+	assert.Equal(big.NewInt(FakeBlockNumber), res.Number)
+	assert.Equal(big.NewInt(FakeL1BlockNumber), res.L1BlockNumber)
+}
diff --git a/eth/blockwatch/testdata/fake_client_block_poller_fixtures.json b/eth/blockwatch/testdata/fake_client_block_poller_fixtures.json
index 7e75e5ba..df628eab 100644
--- a/eth/blockwatch/testdata/fake_client_block_poller_fixtures.json
+++ b/eth/blockwatch/testdata/fake_client_block_poller_fixtures.json
@@ -801,6 +801,48 @@
           "parent": "0x7ac62ad1dc23753eb35ccbe193fdf1ad33828a68ed1c497a17ae66f7baa98144",
           "number": 11
         }
+      },
+      "getBlockByHash": {
+        "0x22a63eebe1b8f4034c3552502c0ba4ffbd613b6ee3cd5b530f75202ddc6d6220": {
+          "hash": "0x22a63eebe1b8f4034c3552502c0ba4ffbd613b6ee3cd5b530f75202ddc6d6220",
+          "parent": "0x2804206056bff166dbeef754799a248c48dd9521b21ee5898b7632837069200c",
+          "number": 5
+        },
+        "0x85fe65e36f4cc457a774cfe500ba7f3efaa6535dd6cb80373b3e91978ecaa01a": {
+          "hash": "0x85fe65e36f4cc457a774cfe500ba7f3efaa6535dd6cb80373b3e91978ecaa01a",
+          "parent": "0x22a63eebe1b8f4034c3552502c0ba4ffbd613b6ee3cd5b530f75202ddc6d6220",
+          "number": 6
+        },
+        "0xc308739706b1cf523663cbd1d8ce01208f7811cc6ff2005d23a13898f2f5adff": {
+          "hash": "0xc308739706b1cf523663cbd1d8ce01208f7811cc6ff2005d23a13898f2f5adff",
+          "parent": "0x85fe65e36f4cc457a774cfe500ba7f3efaa6535dd6cb80373b3e91978ecaa01a",
+          "number": 7
+        },
+        "0x7718297b91827d7a2f9238f4501a5c47cd6a5da83811e53d1cf1231346bd2001": {
+          "hash": "0x7718297b91827d7a2f9238f4501a5c47cd6a5da83811e53d1cf1231346bd2001",
+          "parent": "0xc308739706b1cf523663cbd1d8ce01208f7811cc6ff2005d23a13898f2f5adff",
+          "number": 8
+        },
+        "0x95483b1c35b7a6cdf6e56dc2ac22bda14b26b395823712ac9f881f3639718402": {
+          "hash": "0x95483b1c35b7a6cdf6e56dc2ac22bda14b26b395823712ac9f881f3639718402",
+          "parent": "0x7718297b91827d7a2f9238f4501a5c47cd6a5da83811e53d1cf1231346bd2001",
+          "number": 9
+        },
+        "0x7ac62ad1dc23753eb35ccbe193fdf1ad33828a68ed1c497a17ae66f7baa98144": {
+          "hash": "0x7ac62ad1dc23753eb35ccbe193fdf1ad33828a68ed1c497a17ae66f7baa98144",
+          "parent": "0x95483b1c35b7a6cdf6e56dc2ac22bda14b26b395823712ac9f881f3639718402",
+          "number": 10
+        },
+        "0x42a1a146cee23d156a8525a6ff9e3b41de7f40fb06e58323deb1443da7ba5dc2": {
+          "hash": "0x42a1a146cee23d156a8525a6ff9e3b41de7f40fb06e58323deb1443da7ba5dc2",
+          "parent": "0x7ac62ad1dc23753eb35ccbe193fdf1ad33828a68ed1c497a17ae66f7baa98144",
+          "number": 11
+        },
+        "0xf908916f8ea921df3a6256fc20874577139c9544fd04c7644ff14075bf102b22": {
+          "hash": "0xf908916f8ea921df3a6256fc20874577139c9544fd04c7644ff14075bf102b22",
+          "parent": "0x42a1a146cee23d156a8525a6ff9e3b41de7f40fb06e58323deb1443da7ba5dc2",
+          "number": 12
+        }
       }
     ],
     "scenarioLabel": "REORG_OUT_ALL_RETAINED_BLOCKS"
diff --git a/eth/roundinitializer_test.go b/eth/roundinitializer_test.go
index 7ae1f21a..25e60205 100644
--- a/eth/roundinitializer_test.go
+++ b/eth/roundinitializer_test.go
@@ -102,7 +102,7 @@ func TestRoundInitializer_TryInitialize(t *testing.T) {
 		lastInitializedBlockHash: [32]byte{123},
 	}
 	initializer := NewRoundInitializer(client, tw)
-	initializer.nextRoundStartBlock = big.NewInt(5)
+	initializer.nextRoundStartL1Block = big.NewInt(5)
 	assert := assert.New(t)
 
 	// Test error checking should initialize
@@ -186,7 +186,7 @@ func TestRoundInitializer_Start_Stop(t *testing.T) {
 	err = <-errC
 	assert.Nil(err)
 	// should have set next round start block
-	assert.Equal(initializer.nextRoundStartBlock, big.NewInt(105)) // 100 + 5
+	assert.Equal(initializer.nextRoundStartL1Block, big.NewInt(105)) // 100 + 5
 }
 
 func TestRoundInitializer_RoundSubscription(t *testing.T) {
@@ -214,7 +214,7 @@ func TestRoundInitializer_RoundSubscription(t *testing.T) {
 
 	// Test set next round start block on initializer
 	time.Sleep(1 * time.Second)
-	assert.Equal(initializer.nextRoundStartBlock, big.NewInt(10)) // 5 +5
+	assert.Equal(initializer.nextRoundStartL1Block, big.NewInt(10)) // 5 +5
 	tw.currentRoundStartBlock = big.NewInt(100)
 	tw.roundSink <- types.Log{}
 	time.Sleep(1 * time.Second)
@@ -222,7 +222,7 @@ func TestRoundInitializer_RoundSubscription(t *testing.T) {
 	initializer.Stop()
 	err := <-errC
 	assert.Nil(err)
-	assert.Equal(initializer.nextRoundStartBlock, new(big.Int).Add(roundLength, tw.currentRoundStartBlock))
+	assert.Equal(initializer.nextRoundStartL1Block, new(big.Int).Add(roundLength, tw.currentRoundStartBlock))
 }
 
 func TestRoundInitializer_BlockSubscription(t *testing.T) {
@@ -255,7 +255,7 @@ func TestRoundInitializer_BlockSubscription(t *testing.T) {
 	// block < next round start block do nothing
 	tw.blockSink <- big.NewInt(5)
 	time.Sleep(1 * time.Second)
-	assert.Equal(initializer.nextRoundStartBlock, big.NewInt(10))
+	assert.Equal(initializer.nextRoundStartL1Block, big.NewInt(10))
 
 	// block >= next round start block , try initialize
 	caller := ethcommon.HexToAddress("foo")
@@ -302,7 +302,7 @@ type stubTimeWatcher struct {
 	blockSub  event.Subscription
 }
 
-func (m *stubTimeWatcher) LastSeenBlock() *big.Int {
+func (m *stubTimeWatcher) LastSeenL1Block() *big.Int {
 	return m.lastBlock
 }
 
@@ -310,7 +310,7 @@ func (m *stubTimeWatcher) LastInitializedRound() *big.Int {
 	return m.lastInitializedRound
 }
 
-func (m *stubTimeWatcher) LastInitializedBlockHash() [32]byte {
+func (m *stubTimeWatcher) LastInitializedL1BlockHash() [32]byte {
 	return m.lastInitializedBlockHash
 }
 
@@ -318,7 +318,7 @@ func (m *stubTimeWatcher) GetTranscoderPoolSize() *big.Int {
 	return nil
 }
 
-func (m *stubTimeWatcher) CurrentRoundStartBlock() *big.Int {
+func (m *stubTimeWatcher) CurrentRoundStartL1Block() *big.Int {
 	return m.currentRoundStartBlock
 }
 
@@ -328,7 +328,7 @@ func (m *stubTimeWatcher) SubscribeRounds(sink chan<- types.Log) event.Subscript
 	return m.roundSub
 }
 
-func (m *stubTimeWatcher) SubscribeBlocks(sink chan<- *big.Int) event.Subscription {
+func (m *stubTimeWatcher) SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription {
 	m.blockSink = sink
 	m.blockSub = &stubSubscription{errCh: make(<-chan error)}
 	return m.blockSub
diff --git a/eth/watchers/timewatcher_test.go b/eth/watchers/timewatcher_test.go
index a618719e..73f6123c 100644
--- a/eth/watchers/timewatcher_test.go
+++ b/eth/watchers/timewatcher_test.go
@@ -24,13 +24,13 @@ func TestSetAndGet_LastInitializedRound_LastInitializedBlockHash(t *testing.T) {
 	num := big.NewInt(10)
 	tw.setLastInitializedRound(round, hash, num)
 	assert.Equal(tw.lastInitializedRound, round)
-	assert.Equal(tw.lastInitializedBlockHash, hash)
+	assert.Equal(tw.lastInitializedL1BlockHash, hash)
 
 	r := tw.LastInitializedRound()
 	assert.Equal(r, round)
-	h := tw.LastInitializedBlockHash()
+	h := tw.LastInitializedL1BlockHash()
 	assert.Equal(h, hash)
-	assert.Equal(tw.CurrentRoundStartBlock(), num)
+	assert.Equal(tw.CurrentRoundStartL1Block(), num)
 }
 
 func TestSetAndGet_TranscoderPoolSize(t *testing.T) {
@@ -61,7 +61,7 @@ func TestTimeWatcher_NewTimeWatcher(t *testing.T) {
 		Errors:            make(map[string]error),
 	}
 	watcher := &stubBlockWatcher{
-		latestHeader: &blockwatch.MiniHeader{Number: block},
+		latestHeader: &blockwatch.MiniHeader{L1BlockNumber: block},
 	}
 
 	testErr := fmt.Errorf("error")
@@ -76,13 +76,13 @@ func TestTimeWatcher_NewTimeWatcher(t *testing.T) {
 
 	// BlockHashForRound error
 	lpEth.Errors["BlockHashForRound"] = testErr
-	expErr = fmt.Sprintf("error fetching initial lastInitializedBlockHash value err=%q", testErr)
+	expErr = fmt.Sprintf("error fetching initial lastInitializedL1BlockHash value err=%q", testErr)
 	tw, err = NewTimeWatcher(stubRoundsManagerAddr, watcher, lpEth)
 	assert.Nil(tw)
 	assert.EqualError(err, expErr)
 	lpEth.Errors["BlockHashForRound"] = nil
 
-	// CurrentRoundStartBlock error
+	// CurrentRoundStartL1Block error
 	lpEth.Errors["CurrentRoundStartBlock"] = testErr
 	expErr = fmt.Sprintf("error fetching current round start block err=%q", testErr)
 	tw, err = NewTimeWatcher(stubRoundsManagerAddr, watcher, lpEth)
@@ -108,21 +108,21 @@ func TestTimeWatcher_NewTimeWatcher(t *testing.T) {
 
 	tw, err = NewTimeWatcher(stubRoundsManagerAddr, watcher, lpEth)
 	assert.Nil(err)
-	bh := tw.LastInitializedBlockHash()
+	bh := tw.LastInitializedL1BlockHash()
 	assert.Equal(hash, common.BytesToHash(bh[:]))
 	assert.Equal(round, tw.LastInitializedRound())
 	assert.Equal(size, tw.GetTranscoderPoolSize())
-	assert.Equal(block, tw.LastSeenBlock())
+	assert.Equal(block, tw.LastSeenL1Block())
 
-	// if watcher.GetLatestBlock() == nil, initialise lastSeenBlock to big.NewInt(0)
+	// if watcher.GetLatestBlock() == nil, initialise lastSeenL1Block to big.NewInt(0)
 	watcher.latestHeader = nil
 	tw, err = NewTimeWatcher(stubRoundsManagerAddr, watcher, lpEth)
 	assert.Nil(err)
-	bh = tw.LastInitializedBlockHash()
+	bh = tw.LastInitializedL1BlockHash()
 	assert.Equal(hash, common.BytesToHash(bh[:]))
 	assert.Equal(round, tw.LastInitializedRound())
 	assert.Equal(size, tw.GetTranscoderPoolSize())
-	assert.Equal(big.NewInt(0), tw.LastSeenBlock())
+	assert.Equal(big.NewInt(0), tw.LastSeenL1Block())
 }
 
 func TestTimeWatcher_WatchAndStop(t *testing.T) {
@@ -139,7 +139,7 @@ func TestTimeWatcher_WatchAndStop(t *testing.T) {
 		Errors:            make(map[string]error),
 	}
 	watcher := &stubBlockWatcher{
-		latestHeader: &blockwatch.MiniHeader{Number: block},
+		latestHeader: &blockwatch.MiniHeader{L1BlockNumber: block},
 	}
 	tw, err := NewTimeWatcher(stubRoundsManagerAddr, watcher, lpEth)
 	require.Nil(t, err)
@@ -161,12 +161,12 @@ func TestTimeWatcher_WatchAndStop(t *testing.T) {
 	time.Sleep(2 * time.Millisecond)
 	lastRound := tw.LastInitializedRound()
 	assert.Zero(lastRound.Cmp(big.NewInt(8)))
-	bhForRound := tw.LastInitializedBlockHash()
+	bhForRound := tw.LastInitializedL1BlockHash()
 	var expectedHashForRound [32]byte
 	copy(expectedHashForRound[:], newRoundEvent.Data[:])
 	assert.Equal(bhForRound, expectedHashForRound)
 	assert.Equal(size, tw.GetTranscoderPoolSize())
-	assert.Equal(header.Number, tw.LastSeenBlock())
+	assert.Equal(header.L1BlockNumber, tw.LastSeenL1Block())
 
 	// Test no NewRound events, values on rw remain the same
 	tw.setTranscoderPoolSize(big.NewInt(10))
@@ -175,22 +175,22 @@ func TestTimeWatcher_WatchAndStop(t *testing.T) {
 	time.Sleep(2 * time.Millisecond)
 	lastRound = tw.LastInitializedRound()
 	assert.Zero(lastRound.Cmp(big.NewInt(8)))
-	bhForRound = tw.LastInitializedBlockHash()
+	bhForRound = tw.LastInitializedL1BlockHash()
 	copy(expectedHashForRound[:], newRoundEvent.Data[:])
 	assert.Equal(bhForRound, expectedHashForRound)
 	assert.Equal(big.NewInt(10), tw.GetTranscoderPoolSize())
-	assert.Equal(header.Number, tw.LastSeenBlock())
+	assert.Equal(header.L1BlockNumber, tw.LastSeenL1Block())
 
 	// Test RPC paths (event removed)
 	blockEvent.BlockHeader.Logs = append(blockEvent.BlockHeader.Logs, newRoundEvent)
 	blockEvent.Type = blockwatch.Removed
 	watcher.sink <- []*blockwatch.Event{blockEvent}
 	time.Sleep(2 * time.Millisecond)
-	bhForRound = tw.LastInitializedBlockHash()
+	bhForRound = tw.LastInitializedL1BlockHash()
 	assert.Equal(hash, common.BytesToHash(bhForRound[:]))
 	assert.Equal(round, tw.LastInitializedRound())
 	assert.Equal(size, tw.GetTranscoderPoolSize())
-	assert.Equal(header.Number, tw.LastSeenBlock())
+	assert.Equal(header.L1BlockNumber, tw.LastSeenL1Block())
 
 	// Test Stop
 	tw.Stop()
@@ -213,7 +213,7 @@ func TestTimeWatcher_HandleLog(t *testing.T) {
 	err = tw.handleLog(log)
 	assert.Nil(err)
 	assert.Nil(tw.LastInitializedRound())
-	assert.Equal([32]byte{}, tw.LastInitializedBlockHash())
+	assert.Equal([32]byte{}, tw.LastInitializedL1BlockHash())
 }
 
 func TestLastSeenBlock(t *testing.T) {
@@ -221,21 +221,21 @@ func TestLastSeenBlock(t *testing.T) {
 	tw := &TimeWatcher{}
 	block := big.NewInt(5)
 
-	tw.setLastSeenBlock(block)
-	tw.LastSeenBlock()
-	assert.Equal(big.NewInt(5), tw.LastSeenBlock())
+	tw.setLastSeenL1Block(block)
+	tw.LastSeenL1Block()
+	assert.Equal(big.NewInt(5), tw.LastSeenL1Block())
 }
 
 func TestHandleBlockNum(t *testing.T) {
 	assert := assert.New(t)
 	watcher := &stubBlockWatcher{
-		latestHeader: &blockwatch.MiniHeader{Number: big.NewInt(1)},
+		latestHeader: &blockwatch.MiniHeader{L1BlockNumber: big.NewInt(1)},
 	}
 
 	tw, err := NewTimeWatcher(stubRoundsManagerAddr, watcher, &eth.StubClient{})
 	assert.Nil(err)
 	header := defaultMiniHeader()
-	header.Number = big.NewInt(10)
+	header.L1BlockNumber = big.NewInt(10)
 	blockEvent := &blockwatch.Event{
 		Type:        blockwatch.Added,
 		BlockHeader: header,
@@ -246,26 +246,26 @@ func TestHandleBlockNum(t *testing.T) {
 
 	watcher.sink <- []*blockwatch.Event{blockEvent}
 	time.Sleep(2 * time.Millisecond)
-	assert.Equal(tw.LastSeenBlock(), header.Number)
+	assert.Equal(tw.LastSeenL1Block(), header.L1BlockNumber)
 }
 
 func TestSubscribeBlocks(t *testing.T) {
 	assert := assert.New(t)
 	watcher := &stubBlockWatcher{
-		latestHeader: &blockwatch.MiniHeader{Number: big.NewInt(1)},
+		latestHeader: &blockwatch.MiniHeader{L1BlockNumber: big.NewInt(1)},
 	}
 
 	tw, err := NewTimeWatcher(stubRoundsManagerAddr, watcher, &eth.StubClient{})
 	assert.Nil(err)
 	header := defaultMiniHeader()
-	header.Number = big.NewInt(10)
+	header.L1BlockNumber = big.NewInt(10)
 	blockEvent := &blockwatch.Event{
 		Type:        blockwatch.Added,
 		BlockHeader: header,
 	}
 
 	events := make(chan *big.Int, 10)
-	sub := tw.SubscribeBlocks(events)
+	sub := tw.SubscribeL1Blocks(events)
 	defer sub.Unsubscribe()
 
 	go tw.Watch()
@@ -274,13 +274,13 @@ func TestSubscribeBlocks(t *testing.T) {
 	watcher.sink <- []*blockwatch.Event{blockEvent}
 	time.Sleep(2 * time.Millisecond)
 	update := <-events
-	assert.Equal(update, header.Number)
+	assert.Equal(update, header.L1BlockNumber)
 }
 
 func TestSubscribeRounds(t *testing.T) {
 	assert := assert.New(t)
 	watcher := &stubBlockWatcher{
-		latestHeader: &blockwatch.MiniHeader{Number: big.NewInt(1)},
+		latestHeader: &blockwatch.MiniHeader{L1BlockNumber: big.NewInt(1)},
 	}
 
 	tw, err := NewTimeWatcher(stubRoundsManagerAddr, watcher, &eth.StubClient{})
diff --git a/server/redeemer_test.go b/server/redeemer_test.go
index 67cd29ed..d9b098f9 100644
--- a/server/redeemer_test.go
+++ b/server/redeemer_test.go
@@ -862,7 +862,7 @@ func (m *stubTimeManager) LastInitializedRound() *big.Int {
 	return m.round
 }
 
-func (m *stubTimeManager) LastInitializedBlockHash() [32]byte {
+func (m *stubTimeManager) LastInitializedL1BlockHash() [32]byte {
 	return m.blkHash
 }
 
@@ -870,7 +870,7 @@ func (m *stubTimeManager) GetTranscoderPoolSize() *big.Int {
 	return m.transcoderPoolSize
 }
 
-func (m *stubTimeManager) LastSeenBlock() *big.Int {
+func (m *stubTimeManager) LastSeenL1Block() *big.Int {
 	return m.lastSeenBlock
 }
 
@@ -878,7 +878,7 @@ func (m *stubTimeManager) SubscribeRounds(sink chan<- types.Log) event.Subscript
 	return &stubSubscription{}
 }
 
-func (m *stubTimeManager) SubscribeBlocks(sink chan<- *big.Int) event.Subscription {
+func (m *stubTimeManager) SubscribeL1Blocks(sink chan<- *big.Int) event.Subscription {
 	m.blockNumSink = sink
 	m.blockNumSub = &stubSubscription{errCh: make(chan error)}
 	return m.blockNumSub