reorgsim

package
v0.0.0-...-8c998e4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 2, 2023 License: BSD-3-Clause Imports: 19 Imported by: 0

README

Chain reorg mock code

Package reorgsim provides a very basic mocked superwatcher.EthClient, implemented by struct ReorgSim.

Features

The code in this package can simulate an Ethereum blockchain chain reorgs.

It supports the following chain reorg characteristics:

  1. Changing block hashes after the reorg event (log's TxHash is unchanged, for easier application testing)

  2. Moving logs to new blocks after the reorg event

  3. Multiple reorg events

  4. Backward chain reorgs (i.e. the chain keeps going back to shorter block height)

This should be sufficient for testing superwatcher.Poller, superwatcher.Emitter, and superwatcher.Engine implementations.

Types

  • ReorgSim - mocks superwatcher.Client

  • Block - mocks Ethereum block with main focus on event logs and nothing else

  • BlockChain - represents Ethereum blockchain by mapping a Block to a block number

  • Param - parameters for ReorgSim

  • ReorgEvent - parameters for constructing reorged chains

Variables

  • ErrExitBlockReached - a sentinel error for cleanly exiting ReorgSim. It should be checked for in tests, to break out of the tests once this error is thrown.

Using reorgsim

Struct ReorgSim can handle multiple ReorgEvents, and is the default simulation used by other packages, even though most external test cases still have 1 ReorgEvent for now.

To use code in reorgsim, users will need to prepare logs []types.Log (or map[uint64][]types.Log, where the key is the log's block number) and parameters Param, as well as reorg parameters in []ReorgEvent.

The logs and []ReorgEvent are used to construct mocked blockchains, while Param controls ReorgSim behavior such as initial block number and block progress.

To help with development experience, the package provides convenient functions to read the mocked logs from JSON files. Users can use ethlogfilter to get desired logs in JSON formats.

See code in tests to get a clearer picture of how this works.

Mocked blockchains in package reorgsim

The package defines blockchains (type BlockChain) as a hash map of uint64 to a reference to custom type Block.

Block is a small struct containing only the bare minimum Ethereum block information needed by function poller.mapLogs, so it only needs to have Ethereum logs and the block hashes.

Ethereum transactions and transaction hashes are not considered in reorgsim code, as it is not currently used by the emitter.

To simulate a reorged (forked) blockchain, ReorgSim internally stores multiple BlockChains in reorgSim.reorgedChains. This means that there must be a mechanism for ReorgSim to determine which chain to use for a particular function call.

Deep dive

ReorgEvent

Pass []ReorgEvent to NewBlockChain or NewReorgSim to enable chain reorg simulation. Each event will map directly to a reorged chain.

Each event must have a non-zero uint64 field ReorgEvent.ReorgBlock, which is a pivot point after which block hashes changes (i.e. reorged/forked).

Another property for each event is ReorgEvent.ReorgTrigger, which is a trigger for forking blockchains. This means that after the ReorgSim have seen ReorgTrigger more than once, chain reorg is triggered, and all blocks after ReorgBlock will have different block hashes, and the current chain block drops to ReorgBlock.

If ReorgEvent.ReorgTrigger is 0, then ReorgEvent.ReorgBlock will be used as trigger.

Each event may also optionally have ReorgEvent.MovedLogs, which is a map of a block number to []MoveLogs. The key of map ReorgEvent.MovedLogs is the block number from which the logs are moved.

We specify which logs are moved to which block with []MoveLogs. For each MoveLogs, MoveLogs.TxHashes represent the transaction hashes of the logs we want to move to MoveLogs.NewBlock.

Constructing BlockChain and chain reorg mechanisms

Blockchains can be constructed using NewBlockChain.

The function returns 2 variables, an original chain BlockChain, and reorged chains []BlockChain. The length of reorged chains is identical to the length of []ReorgEvent passed to the constructor(s).

The old chain blocks created with the information in the logs, such as block hashes and transaction hashes.

Things are however different with reorged chains. Up until the first ReorgEvent.ReorgBlock, the reorged chains' blocks have identical data to their counterparts in the old chain from Param.StartBlock.

The reorged chains' blocks after ReorgEvent.ReorgedBlock will be reorged, that is, they will have different block hashes compared to their counterparts in the original chain.

This includes their logs, which will have the reorged block hashes, but the log's transaction hashes and other fields remain unchanged.

New reorged hash is created with PRandomHash(uint64), which takes in a block number and uses that value as a base for new hash.

This means that we can later check if the reorged hash is correct for a particular blockNumber by calling PRandomHash and compare the values, as seen in some tests here.

Implementing superwatcher.EthClient

ReorgSim has 3 public methods for implementing superwatcher.EthClient.

  1. ReorgSim.BlockNumber returns the current internal state ReorgSim.currentBlock In addition to returning currentBlock value, it also increments ReorgSim.currentBlock by Param.BlockProgress in every call. Once currentBlock reaches Param.ExitBlock, it returns ErrExitBlockReached.

  2. ReorgSim.FilterLogs returns event logs from the current chain. In addition to returning event logs, it is the one who triggers chain reorgs by calling ReorgSim.triggerForkChain.

  3. ReorgSim.HeaderByNumber returns the block hash from the current chain.

How ReorgSim triggers chain reorg sequence and forks chains

The logic for triggering a chain reorg with ReorgEvent.ReorgTrigger is in ReorgSim.triggerForkChain(from, to), while he logic for forking chains is in ReorgSim.forkChain().

The main logic here is this - for all blocks before the event ReorgEvent.ReorgedBlock, use logs from the old chain. The logs at or after Param.ReorgedBlock however, will be taken from BOTH the old and the reorged chain, depending on the internal states during each call.

  1. Every call to r.FilterLogs(from, to) will call r.triggerForkChain(from, to).

  2. triggerForkChain(from, to) gets the current ReorgEvent via r.events[r.currentReorgEvent]. It then checks if ReorgEvent.ReorgTrigger is within inclusive range [from, to].

  3. If ReorgEvent.ReorgTrigger is within range [from, to], then triggerForkChain will see if this current event has been triggered. If not triggered (r.currentReorgEvent > r.triggered), then triggerForkChain updates r.triggered to r.currentReorgEvent and returns.

    If it's called again, and it sees that the current event has been triggered (r.currentReorgEvent == r.triggered), then it checks if forked, and if not, calls forkChain

  4. forkChain forks the current chain by overwriting r.chain to r.reorgedChains[r.currentReorgEvent], and incrementing the counters.

  5. After forkChain returns, r.chain should already be forked, and FilterLogs can just access blocks from current chain with r.chain[blockNumber].

  6. The logs within range are appended together and returned.

(Arrow heads are function calls)



ReorgSim.FilterLogs(ctx, query)
                     │
                     │
                     ▼
ReorgSim.triggerForkChain(query.from, query.to)
                     │
                     │
                     │
if current event.ReorgTrigger is in range [query.from, query.to]
                     │
                     │
                     │
          ┌──────────┴────────────────────┐
          │                               │
if not triggered             if already triggered and unforked
          │                               │
          │                               │
          │                               ▼
triggers and returns              ReorgSim.forkChain()
                                          │
                                          │
                                          │
                                   forks and returns

ReorgSim must be able to let poller/emitter see the original hash first (i.e. when ReorgSim.seen for a block is still 0 or 1), otherwise we can test poller.mapLogs, as the function relies on old block hash saved in the tracker

Documentation

Index

Constants

View Source
const NoReorg uint64 = 0

Use this as ReorgEvent.ReorgBlock to disable chain reorg.

Variables

View Source
var (
	DefaultParam = Param{
		BlockProgress: 20,
		Debug:         true,
	}
)
View Source
var ErrExitBlockReached = errors.New("exitBlock reached for reorgsim")

Functions

func InitLogsFromFiles

func InitLogsFromFiles(filenames ...string) []types.Log

func InitMappedLogsFromFiles

func InitMappedLogsFromFiles(filenames ...string) map[uint64][]types.Log

InitMappedLogsFromFiles returns unmarshaled hard-coded logs. It is export for use in internal/emitter testing.

func LogsReorgPaths

func LogsReorgPaths(events []ReorgEvent) ([]common.Hash, map[common.Hash][]uint64, map[common.Hash]uint64)

LogsReorgPaths iterates through |events| to see which blockNumber is the final destination for a log. The return value is a map of log's TX hash to its destination block number, that is, the most current ReorgEvent.

func MapLogsToNumber

func MapLogsToNumber(logs []types.Log) map[uint64][]types.Log

func NewBlockChain

func NewBlockChain(
	logs map[uint64][]types.Log,
	events []ReorgEvent,
) (
	BlockChain,
	[]BlockChain,
)

NewBlockChain is the preferred way to init reorgsim `blockChain`s. It accept a slice of `ReorgEvent` and uses each event to construct a reorged chain, which will be appended to the second return variable. Each ReorgEvent will result in its own blockChain, with the identical index.

func NewReorgSimFromLogs

func NewReorgSimFromLogs(
	param Param,
	events []ReorgEvent,
	logs map[uint64][]types.Log,
	debugName string,
	logLevel uint8,
) (
	superwatcher.EthClient,
	error,
)

func NewReorgSimFromLogsFiles

func NewReorgSimFromLogsFiles(
	param Param,
	events []ReorgEvent,
	logsFiles []string,
	debugName string,
	logLevel uint8,
) (
	superwatcher.EthClient,
	error,
)

func PRandomHash

func PRandomHash(i uint64) common.Hash

PRandomHash returns a deterministic, pseudo-random hash for i

func ReorgHash

func ReorgHash(blockNumber uint64, reorgIndex int) common.Hash

Types

type Block

type Block struct {
	// contains filtered or unexported fields
}

Block represents the Ethereum Block. It is also used as superwatcher.BlockHeader.

func (*Block) GasLimit

func (b *Block) GasLimit() uint64

GasLimit mocks field *types.Header.GasLimit

func (*Block) GasUsed

func (b *Block) GasUsed() uint64

GasUsed mocks field *types.Header.GasUsed

func (*Block) Hash

func (b *Block) Hash() common.Hash

Implements superwatcher.BlockHeader We'll use block in place of *types.Header, because *types.Header is too packed to mock.

func (*Block) Logs

func (b *Block) Logs() []types.Log

func (*Block) Nonce

func (b *Block) Nonce() types.BlockNonce

Nonce mocks field *types.Header.Nonce

func (*Block) Number

func (b *Block) Number() uint64

func (*Block) Time

func (b *Block) Time() uint64

Time mocks field *types.Header.Time

type BlockChain

type BlockChain map[uint64]*Block

type MoveLogs

type MoveLogs struct {
	NewBlock uint64
	TxHashes []common.Hash // txHashes of logs to be moved to newBlock
}

MoveLogs represent a move of logs to a new blockNumber

type Param

type Param struct {
	// StartBlock will be used as initial ReorgSim.currentBlock.
	StartBlock uint64 `json:"startBlock"`
	// BlockProgress is used to increment ReorgSim.currentBlock.
	BlockProgress uint64 `json:"blockProgress"`
	// ExitBlock is checked against ReorgSim.currentBlock for test code to exit at a specify block.
	ExitBlock uint64 `json:"exitBlock"`

	Debug bool `json:"-"`
}

Param is the basic parameters for the mock client. Chain reorg parameters are NOT included here.

type ReorgEvent

type ReorgEvent struct {
	// ReorgTrigger is the block which when seen, triggers a reorg from ReorgBlock
	ReorgTrigger uint64 `json:"reorgTrigger"`
	// ReorgBlock is the pivot block after which ReorgSim should use ReorgSim.reorgedChain.
	ReorgBlock uint64 `json:"reorgBlock"`
	// MovedLogs represents all of the moved logs after a chain reorg event.
	// The map key is the source block number (the block from which the logs are originally in).
	MovedLogs map[uint64][]MoveLogs `json:"movedLogs"`
}

ReorgEvent is parameters for chain reorg events.

type ReorgSim

type ReorgSim struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

ReorgSim is a mock superwatcher.EthClient can simulate multiple on-the-fly chain reorganizations.

func NewReorgSim

func NewReorgSim(
	param Param,
	events []ReorgEvent,
	chain BlockChain,
	reorgedChains []BlockChain,
	debugName string,
	logLevel uint8,
) (
	*ReorgSim,
	error,
)

func (*ReorgSim) BatchCallContext

func (r *ReorgSim) BatchCallContext(ctx context.Context, elems []rpc.BatchElem) error

BatchCallContext only processes `"eth_getBlockByNumber" RPC method calls. Each elem.Result in elems will be overwritten with *Block (implements superwatcher.BlockHeader) from the current chain.

func (*ReorgSim) BlockNumber

func (r *ReorgSim) BlockNumber(ctx context.Context) (uint64, error)

func (*ReorgSim) Chain

func (r *ReorgSim) Chain() BlockChain

func (*ReorgSim) FilterLogs

func (r *ReorgSim) FilterLogs(ctx context.Context, query ethereum.FilterQuery) ([]types.Log, error)

func (*ReorgSim) HeaderByNumber

func (r *ReorgSim) HeaderByNumber(ctx context.Context, number *big.Int) (superwatcher.BlockHeader, error)

func (*ReorgSim) ReorgedChain

func (r *ReorgSim) ReorgedChain(i int) BlockChain

func (*ReorgSim) ReorgedChains

func (r *ReorgSim) ReorgedChains() []BlockChain

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL