Commit 89eab8f7 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #1982 from ethereum-optimism/feat/sequencer-timestamp

l2geth: update timestamp logic
parents 75030843 dad6fd9b
---
'@eth-optimism/l2geth': patch
---
Implement updated timestamp logic
---
'@eth-optimism/integration-tests': patch
---
Update timestamp assertion for new logic
...@@ -2,7 +2,7 @@ import { expect } from './shared/setup' ...@@ -2,7 +2,7 @@ import { expect } from './shared/setup'
/* Imports: External */ /* Imports: External */
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { injectL2Context } from '@eth-optimism/core-utils' import { injectL2Context, expectApprox } from '@eth-optimism/core-utils'
import { predeploys } from '@eth-optimism/contracts' import { predeploys } from '@eth-optimism/contracts'
import { Contract, BigNumber } from 'ethers' import { Contract, BigNumber } from 'ethers'
...@@ -74,9 +74,11 @@ describe('OVM Context: Layer 2 EVM Context', () => { ...@@ -74,9 +74,11 @@ describe('OVM Context: Layer 2 EVM Context', () => {
const l1BlockNumber = await OVMContextStorage.l1BlockNumbers(i) const l1BlockNumber = await OVMContextStorage.l1BlockNumbers(i)
expect(l1BlockNumber.toNumber()).to.deep.equal(l1Block.number) expect(l1BlockNumber.toNumber()).to.deep.equal(l1Block.number)
// L1 and L2 blocks will have the same timestamp. // L1 and L2 blocks will have approximately the same timestamp.
const timestamp = await OVMContextStorage.timestamps(i) const timestamp = await OVMContextStorage.timestamps(i)
expect(timestamp.toNumber()).to.deep.equal(l1Block.timestamp) expectApprox(timestamp.toNumber(), l1Block.timestamp, {
percentUpperDeviation: 5,
})
expect(timestamp.toNumber()).to.deep.equal(l2Block.timestamp) expect(timestamp.toNumber()).to.deep.equal(l2Block.timestamp)
// Difficulty should always be zero. // Difficulty should always be zero.
......
...@@ -33,6 +33,7 @@ export class OptimismEnv { ...@@ -33,6 +33,7 @@ export class OptimismEnv {
addressManager: Contract addressManager: Contract
l1Bridge: Contract l1Bridge: Contract
l1Messenger: Contract l1Messenger: Contract
l1BlockNumber: Contract
ctc: Contract ctc: Contract
scc: Contract scc: Contract
...@@ -59,6 +60,7 @@ export class OptimismEnv { ...@@ -59,6 +60,7 @@ export class OptimismEnv {
this.addressManager = args.addressManager this.addressManager = args.addressManager
this.l1Bridge = args.l1Bridge this.l1Bridge = args.l1Bridge
this.l1Messenger = args.l1Messenger this.l1Messenger = args.l1Messenger
this.l1BlockNumber = args.l1BlockNumber
this.ovmEth = args.ovmEth this.ovmEth = args.ovmEth
this.l2Bridge = args.l2Bridge this.l2Bridge = args.l2Bridge
this.l2Messenger = args.l2Messenger this.l2Messenger = args.l2Messenger
...@@ -113,12 +115,17 @@ export class OptimismEnv { ...@@ -113,12 +115,17 @@ export class OptimismEnv {
.connect(l2Wallet) .connect(l2Wallet)
.attach(predeploys.OVM_SequencerFeeVault) .attach(predeploys.OVM_SequencerFeeVault)
const l1BlockNumber = getContractFactory('iOVM_L1BlockNumber')
.connect(l2Wallet)
.attach(predeploys.OVM_L1BlockNumber)
return new OptimismEnv({ return new OptimismEnv({
addressManager, addressManager,
l1Bridge, l1Bridge,
ctc, ctc,
scc, scc,
l1Messenger, l1Messenger,
l1BlockNumber,
ovmEth, ovmEth,
gasPriceOracle, gasPriceOracle,
sequencerFeeVault, sequencerFeeVault,
......
...@@ -211,4 +211,29 @@ describe('stress tests', () => { ...@@ -211,4 +211,29 @@ describe('stress tests', () => {
) )
}).timeout(STRESS_TEST_TIMEOUT) }).timeout(STRESS_TEST_TIMEOUT)
}) })
// These tests depend on an archive node due to the historical `eth_call`s
describe('Monotonicity Checks', () => {
it('should have monotonic timestamps and l1 blocknumbers', async () => {
const tip = await env.l2Provider.getBlock('latest')
const prev = {
block: await env.l2Provider.getBlock(0),
l1BlockNumber: await env.l1BlockNumber.getL1BlockNumber({
blockTag: 0,
}),
}
for (let i = 1; i < tip.number; i++) {
const block = await env.l2Provider.getBlock(i)
expect(block.timestamp).to.be.gte(prev.block.timestamp)
const l1BlockNumber = await env.l1BlockNumber.getL1BlockNumber({
blockTag: i,
})
expect(l1BlockNumber.gt(prev.l1BlockNumber))
prev.block = block
prev.l1BlockNumber = l1BlockNumber
}
})
})
}) })
...@@ -436,7 +436,7 @@ func (s *SyncService) SequencerLoop() { ...@@ -436,7 +436,7 @@ func (s *SyncService) SequencerLoop() {
} }
s.txLock.Unlock() s.txLock.Unlock()
if err := s.updateContext(); err != nil { if err := s.updateL1BlockNumber(); err != nil {
log.Error("Could not update execution context", "error", err) log.Error("Could not update execution context", "error", err)
} }
} }
...@@ -599,17 +599,15 @@ func (s *SyncService) GasPriceOracleOwnerAddress() *common.Address { ...@@ -599,17 +599,15 @@ func (s *SyncService) GasPriceOracleOwnerAddress() *common.Address {
/// Update the execution context's timestamp and blocknumber /// Update the execution context's timestamp and blocknumber
/// over time. This is only necessary for the sequencer. /// over time. This is only necessary for the sequencer.
func (s *SyncService) updateContext() error { func (s *SyncService) updateL1BlockNumber() error {
context, err := s.client.GetLatestEthContext() context, err := s.client.GetLatestEthContext()
if err != nil { if err != nil {
return err return fmt.Errorf("Cannot get eth context: %w", err)
} }
current := time.Unix(int64(s.GetLatestL1Timestamp()), 0) latest := s.GetLatestL1BlockNumber()
next := time.Unix(int64(context.Timestamp), 0) if context.BlockNumber > latest {
if next.Sub(current) > s.timestampRefreshThreshold { log.Info("Updating L1 block number", "blocknumber", context.BlockNumber)
log.Info("Updating Eth Context", "timetamp", context.Timestamp, "blocknumber", context.BlockNumber)
s.SetLatestL1BlockNumber(context.BlockNumber) s.SetLatestL1BlockNumber(context.BlockNumber)
s.SetLatestL1Timestamp(context.Timestamp)
} }
return nil return nil
} }
...@@ -798,31 +796,61 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error { ...@@ -798,31 +796,61 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error {
return fmt.Errorf("Queue origin L1 to L2 transaction without a timestamp: %s", tx.Hash().Hex()) return fmt.Errorf("Queue origin L1 to L2 transaction without a timestamp: %s", tx.Hash().Hex())
} }
} }
// If there is no OVM timestamp assigned to the transaction, then assign a
// timestamp and blocknumber to it. This should only be the case for queue // If there is no L1 timestamp assigned to the transaction, then assign a
// origin sequencer transactions that come in via RPC. The L1 to L2 // timestamp to it. The property that L1 to L2 transactions have the same
// transactions that come in via `enqueue` should have a timestamp set based // timestamp as the L1 block that it was included in is removed for better
// on the L1 block that it was included in. // UX. This functionality can be added back in during a future release. For
// Note that Ethereum Layer one consensus rules dictate that the timestamp // now, the sequencer will assign a timestamp to each transaction.
// must be strictly increasing between blocks, so no need to check both the
// timestamp and the blocknumber.
ts := s.GetLatestL1Timestamp() ts := s.GetLatestL1Timestamp()
bn := s.GetLatestL1BlockNumber() bn := s.GetLatestL1BlockNumber()
if tx.L1Timestamp() == 0 {
tx.SetL1Timestamp(ts) // The L1Timestamp is 0 for QueueOriginSequencer transactions when
tx.SetL1BlockNumber(bn) // running as the sequencer, the transactions are coming in via RPC.
} else if tx.L1Timestamp() > s.GetLatestL1Timestamp() { // This code path also runs for replicas/verifiers so any logic involving
// If the timestamp of the transaction is greater than the sync // `time.Now` can only run for the sequencer. All other nodes must listen
// service's locally maintained timestamp, update the timestamp and // to what the sequencer says is the timestamp, otherwise there will be a
// blocknumber to equal that of the transaction's. This should happen // network split.
// with `enqueue` transactions. // Note that it should never be possible for the timestamp to be set to
s.SetLatestL1Timestamp(tx.L1Timestamp()) // 0 when running as a verifier.
s.SetLatestL1BlockNumber(tx.L1BlockNumber().Uint64()) shouldMalleateTimestamp := !s.verifier && tx.QueueOrigin() == types.QueueOriginL1ToL2
log.Debug("Updating OVM context based on new transaction", "timestamp", ts, "blocknumber", tx.L1BlockNumber().Uint64(), "queue-origin", tx.QueueOrigin()) if tx.L1Timestamp() == 0 || shouldMalleateTimestamp {
// Get the latest known timestamp
current := time.Unix(int64(ts), 0)
// Get the current clocktime
now := time.Now()
// If enough time has passed, then assign the
// transaction to have the timestamp now. Otherwise,
// use the current timestamp
if now.Sub(current) > s.timestampRefreshThreshold {
current = now
}
tx.SetL1Timestamp(uint64(current.Unix()))
} else if tx.L1Timestamp() == 0 && s.verifier {
// This should never happen
log.Error("No tx timestamp found when running as verifier", "hash", tx.Hash().Hex())
} else if tx.L1Timestamp() < s.GetLatestL1Timestamp() { } else if tx.L1Timestamp() < s.GetLatestL1Timestamp() {
// This should never happen, but sometimes does
log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex()) log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex())
} }
l1BlockNumber := tx.L1BlockNumber()
// Set the L1 blocknumber
if l1BlockNumber == nil {
tx.SetL1BlockNumber(bn)
} else if l1BlockNumber.Uint64() > s.GetLatestL1BlockNumber() {
s.SetLatestL1BlockNumber(l1BlockNumber.Uint64())
} else {
// l1BlockNumber < latest l1BlockNumber
// indicates an error
log.Error("Blocknumber monotonicity violation", "hash", tx.Hash().Hex())
}
// Store the latest timestamp value
if tx.L1Timestamp() > ts {
s.SetLatestL1Timestamp(tx.L1Timestamp())
}
index := s.GetLatestIndex() index := s.GetLatestIndex()
if tx.GetMeta().Index == nil { if tx.GetMeta().Index == nil {
if index == nil { if index == nil {
...@@ -1186,24 +1214,6 @@ func (s *SyncService) syncTransactionRange(start, end uint64, backend Backend) e ...@@ -1186,24 +1214,6 @@ func (s *SyncService) syncTransactionRange(start, end uint64, backend Backend) e
return nil return nil
} }
// updateEthContext will update the OVM execution context's
// timestamp and blocknumber if enough time has passed since
// it was last updated. This is a sequencer only function.
func (s *SyncService) updateEthContext() error {
context, err := s.client.GetLatestEthContext()
if err != nil {
return fmt.Errorf("Cannot get eth context: %w", err)
}
current := time.Unix(int64(s.GetLatestL1Timestamp()), 0)
next := time.Unix(int64(context.Timestamp), 0)
if next.Sub(current) > s.timestampRefreshThreshold {
log.Info("Updating Eth Context", "timetamp", context.Timestamp, "blocknumber", context.BlockNumber)
s.SetLatestL1BlockNumber(context.BlockNumber)
s.SetLatestL1Timestamp(context.Timestamp)
}
return nil
}
// SubscribeNewTxsEvent registers a subscription of NewTxsEvent and // SubscribeNewTxsEvent registers a subscription of NewTxsEvent and
// starts sending event to the given channel. // starts sending event to the given channel.
func (s *SyncService) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription { func (s *SyncService) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription {
......
...@@ -25,71 +25,153 @@ import ( ...@@ -25,71 +25,153 @@ import (
"github.com/ethereum-optimism/optimism/l2geth/rollup/rcfg" "github.com/ethereum-optimism/optimism/l2geth/rollup/rcfg"
) )
func setupLatestEthContextTest() (*SyncService, *EthContext) { // Test that the timestamps are updated correctly.
service, _, _, _ := newTestSyncService(false, nil) // This impacts execution, for `block.timestamp`
resp := &EthContext{ func TestSyncServiceTimestampUpdate(t *testing.T) {
BlockNumber: uint64(10), service, txCh, _, err := newTestSyncService(false, nil)
BlockHash: common.Hash{}, if err != nil {
Timestamp: uint64(service.timestampRefreshThreshold.Seconds()) + 1, t.Fatal(err)
} }
setupMockClient(service, map[string]interface{}{
"GetLatestEthContext": resp,
})
return service, resp // Get the timestamp from the sync service
} // It should be initialized to 0
ts := service.GetLatestL1Timestamp()
if ts != 0 {
t.Fatalf("Unexpected timestamp: %d", ts)
}
// Create a mock transaction and assert that its timestamp
// a value. This tests the case that the timestamp does
// not get malleated when it is set to a non zero value
timestamp := uint64(1)
tx1 := setMockTxL1Timestamp(mockTx(), timestamp)
if tx1.GetMeta().L1Timestamp != timestamp {
t.Fatalf("Expecting mock timestamp to be %d", timestamp)
}
if tx1.GetMeta().QueueOrigin != types.QueueOriginSequencer {
t.Fatalf("Expecting mock queue origin to be queue origin sequencer")
}
go func() {
err = service.applyTransactionToTip(tx1)
}()
event1 := <-txCh
// Test that if applying a transaction fails // Ensure that the timestamp isn't malleated
func TestSyncServiceContextUpdated(t *testing.T) { if event1.Txs[0].GetMeta().L1Timestamp != timestamp {
service, resp := setupLatestEthContextTest() t.Fatalf("Timestamp was malleated: %d", event1.Txs[0].GetMeta().L1Timestamp)
}
// Ensure that the timestamp in the sync service was updated
if service.GetLatestL1Timestamp() != timestamp {
t.Fatal("timestamp updated in sync service")
}
// Now test the case for when a transaction is malleated.
// If the timestamp is 0, then it should be malleated and set
// equal to whatever the latestL1Timestamp is
tx2 := mockTx()
if tx2.GetMeta().L1Timestamp != 0 {
t.Fatal("Expecting mock timestamp to be 0")
}
go func() {
err = service.applyTransactionToTip(tx2)
}()
event2 := <-txCh
// should get the expected context // Ensure that the sync service timestamp is updated
expectedCtx := &OVMContext{ if service.GetLatestL1Timestamp() == 0 {
blockNumber: 0, t.Fatal("timestamp not updated")
timestamp: 0,
} }
// Ensure that the timestamp is malleated to be equal to what the sync
// service has as the latest timestamp
if event2.Txs[0].GetMeta().L1Timestamp != service.GetLatestL1Timestamp() {
t.Fatal("unexpected timestamp update")
}
// L1ToL2 transactions should have their timestamp malleated
// Be sure to set the timestamp to a non zero value so that
// its specifically testing the fact its a deposit tx
tx3 := setMockQueueOrigin(setMockTxL1Timestamp(mockTx(), 100), types.QueueOriginL1ToL2)
// Get a reference to the timestamp before transaction execution
ts3 := service.GetLatestL1Timestamp()
if service.OVMContext != *expectedCtx { go func() {
t.Fatal("context was not instantiated to the expected value") err = service.applyTransactionToTip(tx3)
}()
event3 := <-txCh
if event3.Txs[0].GetMeta().L1Timestamp != ts3 {
t.Fatal("bad malleation")
} }
// Ensure that the timestamp didn't change
if ts3 != service.GetLatestL1Timestamp() {
t.Fatal("timestamp updated when it shouldn't have")
}
}
// run the update context call once // Test that the L1 blocknumber is updated correctly
err := service.updateContext() func TestSyncServiceL1BlockNumberUpdate(t *testing.T) {
service, txCh, _, err := newTestSyncService(false, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// should get the expected context // Get the L1 blocknumber from the sync service
expectedCtx = &OVMContext{ // It should be initialized to 0
blockNumber: resp.BlockNumber, bn := service.GetLatestL1BlockNumber()
timestamp: resp.Timestamp, if bn != 0 {
t.Fatalf("Unexpected timestamp: %d", bn)
}
tx1 := setMockTxL1BlockNumber(mockTx(), new(big.Int).SetUint64(1))
go func() {
err = service.applyTransactionToTip(tx1)
}()
event1 := <-txCh
// Ensure that the L1 blocknumber was not
// malleated
if event1.Txs[0].L1BlockNumber().Uint64() != 1 {
t.Fatal("wrong l1 blocknumber")
} }
if service.OVMContext != *expectedCtx { // Ensure that the latest L1 blocknumber was
t.Fatal("context was not updated to the expected response even though enough time passed") // updated
if service.GetLatestL1BlockNumber() != 1 {
t.Fatal("sync service latest l1 blocknumber not updated")
} }
// updating the context should be a no-op if time advanced by less than // Ensure that a tx without a L1 blocknumber gets one
// the refresh period // assigned
resp.BlockNumber += 1 tx2 := setMockTxL1BlockNumber(mockTx(), nil)
resp.Timestamp += uint64(service.timestampRefreshThreshold.Seconds()) if tx2.L1BlockNumber() != nil {
setupMockClient(service, map[string]interface{}{ t.Fatal("non nil l1 blocknumber")
"GetLatestEthContext": resp, }
}) go func() {
err = service.applyTransactionToTip(tx2)
}()
event2 := <-txCh
// call it again if event2.Txs[0].L1BlockNumber() == nil {
err = service.updateContext() t.Fatal("tx not assigned an l1 blocknumber")
if err != nil { }
t.Fatal(err) if event2.Txs[0].L1BlockNumber().Uint64() != service.GetLatestL1BlockNumber() {
t.Fatal("tx assigned incorrect l1 blocknumber")
} }
// should not get the context from the response because it was too soon // Ensure that the latest L1 blocknumber doesn't go backwards
unexpectedCtx := &OVMContext{ latest := service.GetLatestL1BlockNumber()
blockNumber: resp.BlockNumber, tx3 := setMockTxL1BlockNumber(mockTx(), new(big.Int).SetUint64(latest-1))
timestamp: resp.Timestamp, go func() {
err = service.applyTransactionToTip(tx3)
}()
event3 := <-txCh
if service.GetLatestL1BlockNumber() != latest {
t.Fatal("block number went backwards")
} }
if service.OVMContext == *unexpectedCtx {
t.Fatal("context should not be updated because not enough time passed") if event3.Txs[0].L1BlockNumber().Uint64() != latest-1 {
t.Fatal("l1 block number was malleated")
} }
} }
...@@ -257,20 +339,32 @@ func TestTransactionToTipTimestamps(t *testing.T) { ...@@ -257,20 +339,32 @@ func TestTransactionToTipTimestamps(t *testing.T) {
} }
} }
// Ensure that the timestamp was updated correctly
ts := service.GetLatestL1Timestamp()
if ts != tx2.L1Timestamp() {
t.Fatal("timestamp not updated correctly")
}
// Send a transaction with no timestamp and then let it be updated // Send a transaction with no timestamp and then let it be updated
// by the sync service. This will prevent monotonicity errors as well // by the sync service. This will prevent monotonicity errors as well.
// as give timestamps to queue origin sequencer transactions // as give timestamps to queue origin sequencer transactions
ts := service.GetLatestL1Timestamp() // Ensure that the timestamp is set to `time.Now`
// when it is not set.
tx3 := setMockTxL1Timestamp(mockTx(), 0) tx3 := setMockTxL1Timestamp(mockTx(), 0)
now := time.Now()
go func() { go func() {
err = service.applyTransactionToTip(tx3) err = service.applyTransactionToTip(tx3)
}() }()
result := <-txCh result := <-txCh
service.chainHeadCh <- core.ChainHeadEvent{} service.chainHeadCh <- core.ChainHeadEvent{}
if result.Txs[0].L1Timestamp() != ts { if result.Txs[0].L1Timestamp() != uint64(now.Unix()) {
t.Fatal("Timestamp not updated correctly") t.Fatal("Timestamp not updated correctly")
} }
if service.GetLatestL1Timestamp() != uint64(now.Unix()) {
t.Fatal("latest timestamp not updated correctly")
}
} }
func TestApplyIndexedTransaction(t *testing.T) { func TestApplyIndexedTransaction(t *testing.T) {
...@@ -1036,6 +1130,13 @@ func setMockTxL1Timestamp(tx *types.Transaction, ts uint64) *types.Transaction { ...@@ -1036,6 +1130,13 @@ func setMockTxL1Timestamp(tx *types.Transaction, ts uint64) *types.Transaction {
return tx return tx
} }
func setMockTxL1BlockNumber(tx *types.Transaction, bn *big.Int) *types.Transaction {
meta := tx.GetMeta()
meta.L1BlockNumber = bn
tx.SetTransactionMeta(meta)
return tx
}
func setMockTxIndex(tx *types.Transaction, index uint64) *types.Transaction { func setMockTxIndex(tx *types.Transaction, index uint64) *types.Transaction {
meta := tx.GetMeta() meta := tx.GetMeta()
meta.Index = &index meta.Index = &index
...@@ -1050,6 +1151,13 @@ func setMockQueueIndex(tx *types.Transaction, index uint64) *types.Transaction { ...@@ -1050,6 +1151,13 @@ func setMockQueueIndex(tx *types.Transaction, index uint64) *types.Transaction {
return tx return tx
} }
func setMockQueueOrigin(tx *types.Transaction, qo types.QueueOrigin) *types.Transaction {
meta := tx.GetMeta()
meta.QueueOrigin = qo
tx.SetTransactionMeta(meta)
return tx
}
func newUint64(n uint64) *uint64 { func newUint64(n uint64) *uint64 {
return &n return &n
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment