Commit 63072ca1 authored by Maurelian's avatar Maurelian

op-node: add ToB tests

parent 12dc97ab
......@@ -12,6 +12,7 @@ require (
github.com/ethereum/go-ethereum v1.10.26
github.com/golang/snappy v0.0.4
github.com/google/go-cmp v0.5.8
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
github.com/holiman/uint256 v1.2.0
......
......@@ -250,6 +250,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8 h1:Ep/joEub9YwcjRY6ND3+Y/w0ncE540RtGatVhtZL0/Q=
github.com/google/gofuzz v1.2.1-0.20220503160820-4a35382e8fc8/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
......
package derive
import (
"testing"
"github.com/ethereum-optimism/optimism/op-node/testutils/fuzzerutils"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/assert"
)
// FuzzBatchRoundTrip executes a fuzz test similar to TestBatchRoundTrip, which tests that arbitrary BatchData will be
// encoded and decoded without loss of its original values.
func FuzzBatchRoundTrip(f *testing.F) {
f.Fuzz(func(t *testing.T, fuzzedData []byte) {
// Create our fuzzer wrapper to generate complex values
typeProvider := fuzz.NewFromGoFuzz(fuzzedData).NilChance(0).MaxDepth(10000).NumElements(0, 0x100).AllowUnexportedFields(true)
fuzzerutils.AddFuzzerFunctions(typeProvider)
// Create our batch data from fuzzed data
var batchData BatchData
typeProvider.Fuzz(&batchData)
// Encode our batch data
enc, err := batchData.MarshalBinary()
assert.NoError(t, err)
// Decode our encoded batch data
var dec BatchData
err = dec.UnmarshalBinary(enc)
assert.NoError(t, err)
// Ensure the round trip encoding of batch data did not result in data loss
assert.Equal(t, &batchData, &dec, "round trip batch encoding/decoding did not match original values")
})
}
package derive
import (
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-node/testutils/fuzzerutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/require"
"math/big"
"testing"
)
// fuzzReceipts is similar to makeReceipts except it uses the fuzzer to populate DepositTx fields.
func fuzzReceipts(typeProvider *fuzz.Fuzzer, blockHash common.Hash, depositContractAddr common.Address) (receipts []*types.Receipt, expectedDeposits []*types.DepositTx) {
// Determine how many receipts to generate (capped)
var receiptCount uint64
typeProvider.Fuzz(&receiptCount)
// Cap our receipt count otherwise we might generate for too long and our fuzzer will assume we hung
if receiptCount > 0x10 {
receiptCount = 0x10
}
// Create every receipt we intend to
logIndex := uint(0)
for i := uint64(0); i < receiptCount; i++ {
// Obtain our fuzz parameters for generating this receipt
var txReceiptValues struct {
GoodReceipt bool
DepositLogs []bool
}
typeProvider.Fuzz(&txReceiptValues)
// Generate a list of transaction receipts
var logs []*types.Log
status := types.ReceiptStatusSuccessful
if txReceiptValues.GoodReceipt {
status = types.ReceiptStatusFailed
}
// Determine if this log will be a deposit log or not and generate it accordingly
for _, isDeposit := range txReceiptValues.DepositLogs {
var ev *types.Log
if isDeposit {
// Generate a user deposit source
source := UserDepositSource{L1BlockHash: blockHash, LogIndex: uint64(logIndex)}
// Fuzz parameters to construct our deposit log
var fuzzedDepositInfo struct {
FromAddr *common.Address
ToAddr *common.Address
Value *big.Int
Gas uint64
Data []byte
Mint *big.Int
}
typeProvider.Fuzz(&fuzzedDepositInfo)
// Create our deposit transaction
dep := &types.DepositTx{
SourceHash: source.SourceHash(),
From: *fuzzedDepositInfo.FromAddr,
To: fuzzedDepositInfo.ToAddr,
Value: fuzzedDepositInfo.Value,
Gas: fuzzedDepositInfo.Gas,
Data: fuzzedDepositInfo.Data,
Mint: fuzzedDepositInfo.Mint,
IsSystemTransaction: false,
}
// Marshal our actual log event
ev = MarshalDepositLogEvent(depositContractAddr, dep)
// If we have a good version and our tx succeeded, we add this to our list of expected deposits to
// return.
if status == types.ReceiptStatusSuccessful {
expectedDeposits = append(expectedDeposits, dep)
}
} else {
// If we're generated an unrelated log event (not deposit), fuzz some random parameters to use.
var randomUnrelatedLogInfo struct {
Addr *common.Address
Topics []common.Hash
Data []byte
}
typeProvider.Fuzz(&randomUnrelatedLogInfo)
// Generate the random log
ev = testutils.GenerateLog(*randomUnrelatedLogInfo.Addr, randomUnrelatedLogInfo.Topics, randomUnrelatedLogInfo.Data)
}
ev.TxIndex = uint(i)
ev.Index = logIndex
ev.BlockHash = blockHash
logs = append(logs, ev)
logIndex++
}
// Add our receipt to our list
receipts = append(receipts, &types.Receipt{
Type: types.DynamicFeeTxType,
Status: status,
Logs: logs,
BlockHash: blockHash,
TransactionIndex: uint(i),
})
}
return
}
// FuzzDeriveDepositsRoundTrip tests the derivation of deposits from transaction receipt event logs. It mixes
// valid and invalid deposit transactions and ensures all valid deposits are derived as expected.
// This is a fuzz test corresponding to TestDeriveUserDeposits.
func FuzzDeriveDepositsRoundTrip(f *testing.F) {
f.Fuzz(func(t *testing.T, fuzzedData []byte) {
// Create our fuzzer wrapper to generate complex values
typeProvider := fuzz.NewFromGoFuzz(fuzzedData).NilChance(0).MaxDepth(10000).NumElements(0, 0x100).Funcs(
func(e *big.Int, c fuzz.Continue) {
var temp [32]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
},
func(e *common.Hash, c fuzz.Continue) {
var temp [32]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
},
func(e *common.Address, c fuzz.Continue) {
var temp [20]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
})
// Create a dummy block hash for this block
var blockHash common.Hash
typeProvider.Fuzz(&blockHash)
// Fuzz to generate some random deposit events
receipts, expectedDeposits := fuzzReceipts(typeProvider, blockHash, MockDepositContractAddr)
// Derive our user deposits from the transaction receipts
derivedDeposits, err := UserDeposits(receipts, MockDepositContractAddr)
require.NoError(t, err)
// Ensure all deposits we derived matched what we expected to receive.
require.Equal(t, len(derivedDeposits), len(expectedDeposits))
for i, derivedDeposit := range derivedDeposits {
expectedDeposit := expectedDeposits[i]
require.Equal(t, expectedDeposit, derivedDeposit)
}
})
}
// FuzzDeriveDepositsBadVersion ensures that if a deposit transaction receipt event log specifies an invalid deposit
// version, no deposits should be derived.
func FuzzDeriveDepositsBadVersion(f *testing.F) {
f.Fuzz(func(t *testing.T, fuzzedData []byte) {
// Create our fuzzer wrapper to generate complex values
typeProvider := fuzz.NewFromGoFuzz(fuzzedData).NilChance(0).MaxDepth(10000).NumElements(0, 0x100)
fuzzerutils.AddFuzzerFunctions(typeProvider)
// Create a dummy block hash for this block
var blockHash common.Hash
typeProvider.Fuzz(&blockHash)
// Fuzz to generate some random deposit events
receipts, _ := fuzzReceipts(typeProvider, blockHash, MockDepositContractAddr)
// Loop through all receipt logs and let the fuzzer determine which (if any) to patch.
hasBadDepositVersion := false
for _, receipt := range receipts {
// TODO: Using a hardcoded index (Topics[3]) here is not ideal. The MarshalDepositLogEvent method should
// be spliced apart to be more configurable for these tests.
// Loop for each log in this receipt and check if it has a deposit event from our contract
for _, log := range receipt.Logs {
if log.Address == MockDepositContractAddr && len(log.Topics) >= 4 && log.Topics[0] == DepositEventABIHash {
// Determine if we should set a bad deposit version for this log
var patchBadDeposit bool
typeProvider.Fuzz(&patchBadDeposit)
if patchBadDeposit {
// Generate any topic but the deposit event versions we support.
// TODO: As opposed to keeping this hardcoded, a method such as IsValidVersion(v) should be
// used here.
badTopic := DepositEventVersion0
for badTopic == DepositEventVersion0 {
typeProvider.Fuzz(&badTopic)
}
// Set our bad topic and update our state
log.Topics[3] = badTopic
hasBadDepositVersion = true
}
}
}
}
// Derive our user deposits from the transaction receipts
_, err := UserDeposits(receipts, MockDepositContractAddr)
// If we patched a bad deposit version this iteration, we should expect an error and not be able to proceed
// further
if hasBadDepositVersion {
require.Errorf(t, err, "")
return
}
require.NoError(t, err, "")
})
}
package derive
import (
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum-optimism/optimism/op-node/testutils/fuzzerutils"
fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"math/big"
"math/rand"
"testing"
)
// FuzzParseL1InfoDepositTxDataValid is a fuzz test built from TestParseL1InfoDepositTxData, which constructs random
// L1 deposit tx info and derives a tx from it, then derives the info back from the tx, to ensure round-trip
// derivation is upheld. This generates "valid" data and ensures it is always derived back to original values.
func FuzzParseL1InfoDepositTxDataValid(f *testing.F) {
f.Fuzz(func(t *testing.T, fuzzedData []byte, rngSeed int64) {
// Create our fuzzer wrapper to generate complex values
typeProvider := fuzz.NewFromGoFuzz(fuzzedData).NilChance(0).MaxDepth(10000).NumElements(0, 0x100)
fuzzerutils.AddFuzzerFunctions(typeProvider)
// Generate our fuzzed value types to construct our L1 info
var fuzzVars struct {
InfoBaseFee *big.Int
InfoTime uint64
InfoNum uint64
InfoSequenceNumber uint64
}
typeProvider.Fuzz(&fuzzVars)
// Create an rng provider and construct an L1 info from random + fuzzed data.
rng := rand.New(rand.NewSource(rngSeed))
l1Info := testutils.MakeL1Info(func(l *testutils.MockL1Info) {
l.InfoBaseFee = fuzzVars.InfoBaseFee
l.InfoTime = fuzzVars.InfoTime
l.InfoNum = fuzzVars.InfoNum
l.InfoSequenceNumber = fuzzVars.InfoSequenceNumber
})(rng)
// Create our deposit tx from our info
depTx, err := L1InfoDeposit(l1Info.SequenceNumber(), l1Info)
require.NoError(t, err)
// Get our info from out deposit tx
res, err := L1InfoDepositTxData(depTx.Data)
require.NoError(t, err, "expected valid deposit info")
// Verify all parameters match in our round trip deriving operations
assert.Equal(t, res.Number, l1Info.NumberU64())
assert.Equal(t, res.Time, l1Info.Time())
assert.True(t, res.BaseFee.Sign() >= 0)
assert.Equal(t, res.BaseFee.Bytes(), l1Info.BaseFee().Bytes())
assert.Equal(t, res.BlockHash, l1Info.Hash())
})
}
// FuzzParseL1InfoDepositTxDataBadLength is a fuzz test built from TestParseL1InfoDepositTxData, which constructs
// random L1 deposit tx info and derives a tx from it, then derives the info back from the tx, to ensure round-trip
// derivation is upheld. This generates "invalid" data and ensures it always throws an error where expected.
func FuzzParseL1InfoDepositTxDataBadLength(f *testing.F) {
const expectedDepositTxDataLength = 4 + 32 + 32 + 32 + 32 + 32
f.Fuzz(func(t *testing.T, fuzzedData []byte) {
// Derive a transaction from random fuzzed data
_, err := L1InfoDepositTxData(fuzzedData)
// If the data is null, or too short or too long, we expect an error
if fuzzedData == nil || len(fuzzedData) != expectedDepositTxDataLength {
assert.Error(t, err)
}
})
}
package driver
import (
"context"
"errors"
"github.com/ethereum-optimism/optimism/op-node/eth"
"github.com/ethereum-optimism/optimism/op-node/metrics"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
"github.com/ethereum-optimism/optimism/op-node/testutils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"math/rand"
"testing"
)
type TestDummyOutputImpl struct {
willError bool
outputInterface
}
func (d TestDummyOutputImpl) createNewBlock(ctx context.Context, l2Head eth.L2BlockRef, l2SafeHead eth.BlockID, l2Finalized eth.BlockID, l1Origin eth.L1BlockRef) (eth.L2BlockRef, *eth.ExecutionPayload, error) {
// If we're meant to error, return one
if d.willError {
return l2Head, nil, errors.New("the TestDummyOutputImpl.createNewBlock operation failed")
}
payload := eth.ExecutionPayload{
ParentHash: common.Hash{},
FeeRecipient: common.Address{},
StateRoot: eth.Bytes32{},
ReceiptsRoot: eth.Bytes32{},
LogsBloom: eth.Bytes256{},
PrevRandao: eth.Bytes32{},
BlockNumber: 0,
GasLimit: 0,
GasUsed: 0,
Timestamp: 0,
ExtraData: nil,
BaseFeePerGas: eth.Uint256Quantity{},
BlockHash: common.Hash{},
Transactions: []eth.Data{},
}
return l2Head, &payload, nil
}
type TestDummyDerivationPipeline struct {
DerivationPipeline
}
func (d TestDummyDerivationPipeline) Reset() {}
func (d TestDummyDerivationPipeline) Step(ctx context.Context) error { return nil }
func (d TestDummyDerivationPipeline) SetUnsafeHead(head eth.L2BlockRef) {}
func (d TestDummyDerivationPipeline) AddUnsafePayload(payload *eth.ExecutionPayload) {}
func (d TestDummyDerivationPipeline) Finalized() eth.L2BlockRef { return eth.L2BlockRef{} }
func (d TestDummyDerivationPipeline) SafeL2Head() eth.L2BlockRef { return eth.L2BlockRef{} }
func (d TestDummyDerivationPipeline) UnsafeL2Head() eth.L2BlockRef { return eth.L2BlockRef{} }
func (d TestDummyDerivationPipeline) Progress() derive.Progress {
return derive.Progress{
Origin: eth.L1BlockRef{},
Closed: false,
}
}
// TestRejectCreateBlockBadTimestamp tests that a block creation with invalid timestamps will be caught.
// This does not test:
// - The findL1Origin call (it is hardcoded to be the head)
// - The outputInterface used to create a new block from a given payload.
// - The DerivationPipeline setting unsafe head (a mock provider is used to pretend to set it)
// - Metrics (only mocked enough to let the method proceed)
// - Publishing (network is set to nil so publishing won't occur)
func TestRejectCreateBlockBadTimestamp(t *testing.T) {
// Create our random provider
rng := rand.New(rand.NewSource(rand.Int63()))
// Create our context for methods to execute under
ctx := context.Background()
// Create our fake L1/L2 heads and link them accordingly
l1HeadRef := testutils.RandomBlockRef(rng)
l2HeadRef := testutils.RandomL2BlockRef(rng)
l2l1OriginBlock := l1HeadRef
l2HeadRef.L1Origin = l2l1OriginBlock.ID()
// Create a rollup config
cfg := rollup.Config{
BlockTime: uint64(60),
Genesis: rollup.Genesis{
L1: l1HeadRef.ID(),
L2: l2HeadRef.ID(),
L2Time: 0x7000, // dummy value
},
}
// Patch our timestamp so we fail
l2HeadRef.Time = l2l1OriginBlock.Time - (cfg.BlockTime * 2)
// Create our outputter
outputProvider := TestDummyOutputImpl{willError: false}
// Create our state
s := state{
l1Head: l1HeadRef,
l2Head: l2HeadRef,
l2SafeHead: l2HeadRef,
l2Finalized: l2HeadRef,
Config: &cfg,
log: log.New(),
output: outputProvider,
derivation: TestDummyDerivationPipeline{},
metrics: &metrics.Metrics{TransactionsSequencedTotal: prometheus.NewCounter(prometheus.CounterOpts{})},
}
// Create a new block
// - L2Head's L1Origin, its timestamp should be greater than L1 genesis.
// - L2Head timestamp + BlockTime should be greater than or equal to the L1 Time.
err := s.createNewL2Block(ctx)
// Verify the L1Origin's timestamp is greater than L1 genesis in our config.
if l2l1OriginBlock.Number < s.Config.Genesis.L1.Number {
assert.Nil(t, err)
return
}
// Verify the new L2 block to create will have a time stamp equal or newer than our L1 origin block we derive from.
if l2HeadRef.Time+cfg.BlockTime < l2l1OriginBlock.Time {
// If not, we expect a specific error.
// TODO: This isn't the cleanest, we should construct + compare the whole error message.
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "cannot build L2 block on top")
assert.Contains(t, err.Error(), "for time")
assert.Contains(t, err.Error(), "before L1 origin")
return
}
// Otherwise we should have no error.
assert.Nil(t, err)
// If we expected the outputter to error, capture that here
if outputProvider.willError {
assert.NotNil(t, err, "outputInterface failed to createNewBlock, so createNewL2Block should also have failed")
return
}
// Otherwise we should have no error.
assert.Nil(t, err)
}
// FuzzRejectCreateBlockBadTimestamp is a property test derived from the TestRejectCreateBlockBadTimestamp unit test.
// It fuzzes timestamps and block times to find a configuration to violate error checking.
func FuzzRejectCreateBlockBadTimestamp(f *testing.F) {
f.Fuzz(func(t *testing.T, randSeed int64, l2Time uint64, blockTime uint64, forceOutputFail bool, currentL2HeadTime uint64) {
// Create our random provider
rng := rand.New(rand.NewSource(randSeed))
// Create our context for methods to execute under
ctx := context.Background()
// Create our fake L1/L2 heads and link them accordingly
l1HeadRef := testutils.RandomBlockRef(rng)
l2HeadRef := testutils.RandomL2BlockRef(rng)
l2l1OriginBlock := l1HeadRef
l2HeadRef.L1Origin = l2l1OriginBlock.ID()
// TODO: Cap our block time so it doesn't overflow
if blockTime > 0x100000 {
blockTime = 0x100000
}
// Create a rollup config
cfg := rollup.Config{
BlockTime: blockTime,
Genesis: rollup.Genesis{
L1: l1HeadRef.ID(),
L2: l2HeadRef.ID(),
L2Time: l2Time, // dummy value
},
}
// Patch our timestamp so we fail
l2HeadRef.Time = currentL2HeadTime
// Create our outputter
outputProvider := TestDummyOutputImpl{willError: forceOutputFail}
// Create our state
s := state{
l1Head: l1HeadRef,
l2Head: l2HeadRef,
l2SafeHead: l2HeadRef,
l2Finalized: l2HeadRef,
Config: &cfg,
log: log.New(),
output: outputProvider,
derivation: TestDummyDerivationPipeline{},
metrics: &metrics.Metrics{TransactionsSequencedTotal: prometheus.NewCounter(prometheus.CounterOpts{})},
}
// Create a new block
// - L2Head's L1Origin, its timestamp should be greater than L1 genesis.
// - L2Head timestamp + BlockTime should be greater than or equal to the L1 Time.
err := s.createNewL2Block(ctx)
// Verify the L1Origin's timestamp is greater than L1 genesis in our config.
if l2l1OriginBlock.Number < s.Config.Genesis.L1.Number {
assert.Nil(t, err)
return
}
// Verify the new L2 block to create will have a time stamp equal or newer than our L1 origin block we derive from.
if l2HeadRef.Time+cfg.BlockTime < l2l1OriginBlock.Time {
// If not, we expect a specific error.
// TODO: This isn't the cleanest, we should construct + compare the whole error message.
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "cannot build L2 block on top")
assert.Contains(t, err.Error(), "for time")
assert.Contains(t, err.Error(), "before L1 origin")
return
}
// Otherwise we should have no error.
assert.Nil(t, err)
// If we expected the outputter to error, capture that here
if outputProvider.willError {
assert.NotNil(t, err, "outputInterface failed to createNewBlock, so createNewL2Block should also have failed")
return
}
// Otherwise we should have no error.
assert.Nil(t, err)
})
}
package fuzzerutils
import (
"math/big"
"github.com/ethereum/go-ethereum/common"
fuzz "github.com/google/gofuzz"
)
// AddFuzzerFunctions takes a fuzz.Fuzzer and adds a list of functions to handle different
// data types in a fuzzing campaign. It adds support for commonly used types throughout the
// application.
func AddFuzzerFunctions(fuzzer *fuzz.Fuzzer) {
fuzzer.Funcs(
func(e *big.Int, c fuzz.Continue) {
var temp [32]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
},
func(e *common.Hash, c fuzz.Continue) {
var temp [32]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
},
func(e *common.Address, c fuzz.Continue) {
var temp [20]byte
c.Fuzz(&temp)
e.SetBytes(temp[:])
},
)
}
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