ethmonitor

package
v1.46.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: LGPL-3.0, MIT Imports: 29 Imported by: 2

README

ethmonitor

Follows an EVM chain block by block and publishes each new block, and optionally its logs, to subscribers. Reorgs come through as explicit events.

ethmonitor keeps a short canonical chain in memory. When the node's view of the chain changes, it emits Removed events for the orphaned blocks and Added events for their replacements, in order, so a subscriber can keep its own state consistent with the chain.

Usage

provider, err := ethrpc.NewProvider("https://your-node-url")
if err != nil {
	return err
}

opts := ethmonitor.DefaultOptions
opts.WithLogs = true

monitor, err := ethmonitor.NewMonitor(provider, opts)
if err != nil {
	return err
}

go func() {
	if err := monitor.Run(ctx); err != nil {
		// Run returns when ctx is cancelled, Stop is called, or on a fatal error
	}
}()
defer monitor.Stop()

sub := monitor.Subscribe("my-service")
defer sub.Unsubscribe()

for {
	select {
	case blocks := <-sub.Blocks():
		for _, block := range blocks {
			switch block.Event {
			case ethmonitor.Added:
				// a new canonical block; block.Logs is set when WithLogs is on
			case ethmonitor.Removed:
				// a reorged block: undo whatever was done for its Added event
			}
		}
	case <-sub.Done():
		return sub.Err()
	}
}

To get new heads over a websocket instead of polling, pass ethrpc.WithStreaming(wsURL) to ethrpc.NewProvider. The monitor falls back to polling if streaming keeps failing (see Options).

How it works

 new head (poll or stream)
        │
        ▼
 fetch next block ──► canonical chain ──► fetch logs ──► publish queue ──► subscribers
                      (last N blocks,     (WithLogs)     (in order,
                       reorg detection)                   ready blocks only)
  1. Fetch. The monitor fetches the next block by number.
  2. Chain. If the block's parent is the current head, it's added to the in-memory chain. If not, the monitor walks back to the common ancestor, emitting Removed for each orphaned block, then adds the new branch. The chain keeps the last BlockRetentionLimit + TrailNumBlocksBehindHead blocks, and never fewer than 10.
  3. Logs. With WithLogs, the block's logs are fetched with eth_getLogs by block hash and validated before the block counts as ready (Block.OK). See Logs.
  4. Publish. Events are queued and published in order, and publishing stops at the first block that isn't ready yet. With TrailNumBlocksBehindHead, events are held until they are that many blocks behind the head, which cuts down on Added/Removed churn on chains that reorg often.
Events

Each Blocks batch a subscription receives holds one or more *Block events, oldest first:

  • Added: the block joined the canonical chain.
  • Removed: a block that was published as Added is no longer canonical. Match it by block hash.

Blocks.Reorg() reports whether a batch contains a removal. Consumers that store anything per block must handle Removed.

Reading chain state

The monitor also answers questions about the chain it holds in memory:

  • Latest blocks:
    • LatestBlock() returns the head of the in-memory chain.
    • LatestReadyBlock() returns the newest block whose data (including logs) is complete.
    • LatestFinalBlock(n) returns the block n blocks behind the head.
  • Lookups: GetBlock(hash) and GetTransaction(hash) search the retained blocks.
  • Chain statistics: GetAverageBlockTime(), and FeeHistory(...), an approximation of eth_feeHistory computed from the retained blocks without fetching receipts. It uses gas limits and priority fees in place of gas used and effective priority fees.
  • The chain itself: Chain() gives access to it directly.

Logs

With WithLogs, every published Added block carries its logs in Block.Logs. LogTopics limits them to logs whose first topic is in the list.

How logs are validated

A node can answer eth_getLogs with [] before it has indexed a block, so an empty result isn't always trusted:

  • Most chains: if eth_getLogs returns [] but the header's logsBloom is non-zero, the result is rejected and retried, since the bloom says the block has logs.

  • Chains with settled headers: on Avalanche C-Chain after ACP-194 (Helicon), the logsBloom describes the settled blocks rather than the block itself, so it can't be used. The monitor detects these chains from the headers' settledHeight field, with no configuration needed. It confirms an empty result against the block's receipts, which a node only serves once the block has executed. If the receipts have logs, those are used.

    Note: this check only runs when LogTopics is empty. With a topic filter set, an empty result on these chains is accepted without confirmation, so a premature one can drop a block's logs. On settled-header chains, leave LogTopics empty and filter the logs yourself.

  • Receipts methods: receipts are fetched with eth_getBlockReceipts. If the node says the method doesn't exist, the monitor switches to eth_getTransactionReceipt per transaction, in batches of 20, plus eth_getBlockTransactionCountByHash. It uses that path for the rest of its lifetime.

When logs keep failing

A block whose logs can't be fetched blocks everything after it, because publishing is in order. To keep the monitor moving:

  1. Receipts fallback. After MaxLogsAttempts failed attempts (default 10), the block's logs are taken from its receipts. On chains without settled headers, empty receipts for a block with a non-zero bloom are still rejected.
  2. Leaving the chain cache. If the block is still unresolved when it leaves the chain cache (BlockRetentionLimit + TrailNumBlocksBehindHead blocks later), one more receipts attempt is made. If that fails too:
    • Default: the block is published with empty Logs and Block.LogsUnconfirmed = true. An Error is logged and the Alerter is called.
    • StrictLogs: true: the block is never published without confirmed logs. The monitor keeps retrying, and when the publish queue (2 × (BlockRetentionLimit + TrailNumBlocksBehindHead) events) overflows, Run returns ErrFatal wrapping ErrQueueFull.

Which mode to use: keep the default when a short gap is better than downtime, for example live notifications. Use StrictLogs when a missing log is worse than stopping. For example, an indexer that persists what it receives would rather restart from its last checkpoint than record a block as having no events.

Options

Start from ethmonitor.DefaultOptions and override what you need.

Option Default What it does
PollingInterval 1.5s How often to poll for new blocks.
StreamingDisabled false Poll even if the provider has a websocket URL.
StreamingErrNumToSwitchToPolling 3 Streaming errors before switching to polling.
StreamingErrorResetInterval 2m How often the streaming error count resets.
StreamingRetryAfter 5m How long to poll before retrying streaming.
Timeout 20s Timeout for node requests.
StartBlockNumber nil (latest) Block to start from. A negative value starts that many blocks behind the latest.
Bootstrap false Start from a chain restored with Chain().BootstrapFromBlocks / BootstrapFromBlocksJSON. Takes precedence over StartBlockNumber.
TrailNumBlocksBehindHead 0 Publish only once a block is this many blocks behind the head.
BlockRetentionLimit 200 Blocks kept in the in-memory chain, on top of TrailNumBlocksBehindHead. This is also the deepest reorg the monitor can follow. Keep the sum above 5: smaller values can overflow the publish queue.
WithLogs false Fetch and publish each block's logs.
LogTopics all Only include logs whose first topic is in this list.
MaxLogsAttempts 10 Failed eth_getLogs attempts before falling back to receipts.
StrictLogs false Never publish a block without confirmed logs (see Logs).
PrefetchConcurrency 0 (off) Workers fetching blocks and logs ahead of the monitor while it trails the head. Useful on fast chains.
PrefetchWindow 4 × concurrency How far ahead prefetching may go.
CacheBackend nil Shared cache for block and log payloads. In most cases, leave it nil.
CacheExpiry 10m Lifetime of cache entries.
UnsubscribeOnStop false Close all subscriptions when Run returns.
ChainID from node Set it to skip asking the node.
Alerter no-op Called on conditions that need attention, such as a block published without confirmed logs.
Logger discard *slog.Logger for warnings and debug output.
DebugLogging false Verbose logging.

Restarts and snapshots

Chain().Snapshot() serializes the retained chain to JSON. A new monitor created with Bootstrap: true can then be restored with Chain().BootstrapFromBlocksJSON(data) (or BootstrapFromBlocks) before Run. It continues from the snapshot's head and can still follow reorgs into the restored blocks.

Errors

Run returns when its context is cancelled, when Stop is called, or on an error the monitor can't recover from. Fatal errors wrap ErrFatal. ErrQueueFull is the main one: the publish queue overflowed. In default mode that only happens for reorgs deeper than the chain cache, or when BlockRetentionLimit + TrailNumBlocksBehindHead is 5 or less.

A subscription's Done() channel closes when it ends, and Err() returns the reason.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrFatal                 = errors.New("ethmonitor: fatal error, stopping")
	ErrReorg                 = errors.New("ethmonitor: block reorg")
	ErrUnexpectedParentHash  = errors.New("ethmonitor: unexpected parent hash")
	ErrUnexpectedBlockNumber = errors.New("ethmonitor: unexpected block number")
	ErrQueueFull             = errors.New("ethmonitor: publish queue is full")
	ErrMaxAttempts           = errors.New("ethmonitor: max attempts hit")
	ErrMonitorStopped        = errors.New("ethmonitor: stopped")
)
View Source
var DefaultOptions = Options{
	Logger:                           slog.New(slog.NewTextHandler(io.Discard, nil)),
	PollingInterval:                  1500 * time.Millisecond,
	StreamingErrorResetInterval:      2 * time.Minute,
	StreamingRetryAfter:              5 * time.Minute,
	StreamingErrNumToSwitchToPolling: 3,
	StreamingDisabled:                false,
	UnsubscribeOnStop:                false,
	Timeout:                          20 * time.Second,
	StartBlockNumber:                 nil,
	TrailNumBlocksBehindHead:         0,
	BlockRetentionLimit:              200,
	WithLogs:                         false,
	LogTopics:                        []common.Hash{},
	DebugLogging:                     false,
	CacheExpiry:                      600 * time.Second,
	PrefetchConcurrency:              0,
	PrefetchWindow:                   0,
	Alerter:                          util.NoopAlerter(),
	MaxLogsAttempts:                  10,
	StrictLogs:                       false,
}

Functions

func CacheKeyBlockByHash added in v1.35.6

func CacheKeyBlockByHash(chainID *big.Int, hash common.Hash) string

func CacheKeyBlockByNumber added in v1.35.6

func CacheKeyBlockByNumber(chainID *big.Int, num *big.Int) string

func CacheKeyBlockLogs added in v1.35.6

func CacheKeyBlockLogs(chainID *big.Int, blockHash common.Hash, topics [][]common.Hash) string

func IsBlockEq added in v1.7.0

func IsBlockEq(a, b *types.Block) bool

Types

type Block

type Block struct {
	*types.Block

	// Event type where Block is Added or Removed (ie. reorged)
	Event Event

	// Logs in the block, grouped by transactions:
	// [[txnA logs, ..], [txnB logs, ..], ..]
	// Logs [][]types.Log `json:"logs"`
	Logs []types.Log

	// OK flag which represents the block is ready for broadcasting
	OK bool

	// LogsUnconfirmed is set when the block was published without confirmed
	// logs: eth_getLogs and receipts both failed for its whole stay in the
	// chain cache. Logs is empty, but the block may have had logs. Never set
	// when StrictLogs is enabled.
	LogsUnconfirmed bool
	// contains filtered or unexported fields
}

Block contains a monitored block and its event data. Construct values with keyed composite literals: private canonical state makes positional literals unsupported.

func (*Block) CanonicalState added in v1.44.0

func (b *Block) CanonicalState() (incarnation uint64, canonical bool)

CanonicalState reports the monitor-assigned incarnation and whether it has remained canonical without a known removal. Zero means the block is untracked. Retention eviction preserves this state; in-memory copies share removal updates. Incarnations are local to a monitor chain. Serialized blocks are untracked until accepted by a monitor.

func (*Block) MarshalJSON added in v1.17.9

func (b *Block) MarshalJSON() ([]byte, error)

func (*Block) UnmarshalJSON added in v1.17.9

func (b *Block) UnmarshalJSON(data []byte) error

type Blocks

type Blocks []*Block

func (Blocks) Copy added in v1.3.0

func (blocks Blocks) Copy() Blocks

func (Blocks) EventExists added in v1.3.0

func (blocks Blocks) EventExists(block *types.Block, event Event) bool

func (Blocks) FeeHistory added in v1.42.2

func (b Blocks) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error)

FeeHistory approximates the behaviour of eth_feeHistory. It intentionally deviates from standard to avoid fetching transaction receipts. It uses transaction gas limits instead of actual gas used, and transaction priority fees instead of effective priority fees.

func (Blocks) FindBlock

func (blocks Blocks) FindBlock(blockHash common.Hash, optEvent ...Event) (*Block, bool)

func (Blocks) Head added in v1.7.0

func (b Blocks) Head() *Block

func (Blocks) IsOK added in v1.7.0

func (b Blocks) IsOK() bool

func (Blocks) LatestBlock

func (b Blocks) LatestBlock() *Block

func (Blocks) Reorg added in v1.7.0

func (b Blocks) Reorg() bool

func (Blocks) Tail added in v1.7.0

func (b Blocks) Tail() *Block

type Chain

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

func (*Chain) Blocks

func (c *Chain) Blocks() Blocks

func (*Chain) BootstrapFromBlocks added in v1.17.9

func (c *Chain) BootstrapFromBlocks(blocks []*Block) error

BootstrapFromBlocks will bootstrap the ethmonitor canonical chain from input blocks, while also verifying the chain hashes link together.

func (*Chain) BootstrapFromBlocksJSON added in v1.17.9

func (c *Chain) BootstrapFromBlocksJSON(data []byte) error

BootstrapFromBlocksJSON is convenience method which accepts json and bootstraps the ethmonitor chain. This method is here mostly for debugging purposes and recommend that you use BootstrapFromBlocks and handle constructing block events outside of ethmonitor.

func (*Chain) GetAverageBlockTime added in v1.14.2

func (c *Chain) GetAverageBlockTime() float64

func (*Chain) GetBlock

func (c *Chain) GetBlock(hash common.Hash) *Block

func (*Chain) GetBlockByNumber added in v1.3.2

func (c *Chain) GetBlockByNumber(blockNum uint64, event Event) *Block

func (*Chain) GetTransaction

func (c *Chain) GetTransaction(txnHash common.Hash) (*types.Transaction, Event)

GetTransaction searches our canonical chain of blocks (where each block points at previous), and returns the transaction. Aka, searches our chain for mined transactions. Keep in mind transactions can still be reorged, but you can check the blockNumber and compare it against the head to determine if its final.

func (*Chain) Head

func (c *Chain) Head() *Block

func (*Chain) PrintAllBlocks

func (c *Chain) PrintAllBlocks()

func (*Chain) ReadyHead added in v1.22.0

func (c *Chain) ReadyHead() *Block

func (*Chain) Snapshot added in v1.17.9

func (c *Chain) Snapshot() ([]byte, error)

func (*Chain) Tail added in v1.7.0

func (c *Chain) Tail() *Block

type Event added in v1.3.0

type Event uint32
const (
	Added Event = iota
	Removed
)

type Monitor

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

func NewMonitor

func NewMonitor(provider ethrpc.RawInterface, options ...Options) (*Monitor, error)

func (*Monitor) Chain

func (m *Monitor) Chain() *Chain

func (*Monitor) FeeHistory added in v1.42.2

func (m *Monitor) FeeHistory(ctx context.Context, blockCount uint64, lastBlock *big.Int, rewardPercentiles []float64) (*ethereum.FeeHistory, error)

FeeHistory approximates the behaviour of eth_feeHistory. It intentionally deviates from standard to avoid fetching transaction receipts. It uses transaction gas limits instead of actual gas used, and transaction priority fees instead of effective priority fees.

func (*Monitor) GetAverageBlockTime added in v1.14.2

func (m *Monitor) GetAverageBlockTime() float64

GetAverageBlockTime returns the average block time in seconds (including fractions)

func (*Monitor) GetBlock

func (m *Monitor) GetBlock(blockHash common.Hash) *Block

GetBlock will search the retained blocks for the hash

func (*Monitor) GetTransaction

func (m *Monitor) GetTransaction(txnHash common.Hash) (*types.Transaction, Event)

GetTransaction will search within the retained canonical chain for the given transaction hash and will only return transactions which have not been removed from the chain via a reorg.

func (*Monitor) IsRunning

func (m *Monitor) IsRunning() bool

func (*Monitor) IsStreamingEnabled added in v1.29.3

func (m *Monitor) IsStreamingEnabled() bool

func (*Monitor) IsStreamingMode added in v1.29.3

func (m *Monitor) IsStreamingMode() bool

func (*Monitor) LatestBlock

func (m *Monitor) LatestBlock() *Block

LatestBlock will return the head block of the canonical chain

func (*Monitor) LatestBlockNum added in v1.17.0

func (m *Monitor) LatestBlockNum() *big.Int

LatestBlockNum returns the latest block number in the canonical chain

func (*Monitor) LatestFinalBlock added in v1.17.2

func (m *Monitor) LatestFinalBlock(numBlocksToFinality int) *Block

LatestFinalBlock returns the latest block which has reached finality. The argument `numBlocksToFinality` should be a constant value of the number of blocks a particular chain needs to reach finality. Ie. on Polygon this value would be 120 and on Ethereum it would be 20. As the pubsub system publishes new blocks, this value will change, as the chain will progress forward. It's recommend / safe to call this method each time in a <-sub.Blocks() code block.

func (*Monitor) LatestReadyBlock added in v1.22.0

func (m *Monitor) LatestReadyBlock() *Block

LatestReadyBlock returns the latest block in the canonical chain which has block.OK state to true, as in all details are available for the block.

func (*Monitor) LatestReadyBlockNum added in v1.22.0

func (m *Monitor) LatestReadyBlockNum() *big.Int

LatestReadyBlockNum returns the latest block number in the canonical chain which has block.OK state to true, as in all details are available for the block.

func (*Monitor) NumSubscribers added in v1.19.4

func (m *Monitor) NumSubscribers() int

func (*Monitor) OldestBlockNum added in v1.17.0

func (m *Monitor) OldestBlockNum() *big.Int

func (*Monitor) Options

func (m *Monitor) Options() Options

func (*Monitor) Provider added in v1.1.10

func (m *Monitor) Provider() ethrpc.Interface

func (*Monitor) PurgeHistory added in v1.17.0

func (m *Monitor) PurgeHistory()

PurgeHistory clears all but the head of the chain. Useful for tests, but should almost never be used in a normal application.

func (*Monitor) Run added in v1.6.1

func (m *Monitor) Run(ctx context.Context) error

func (*Monitor) Stop

func (m *Monitor) Stop()

func (*Monitor) Subscribe

func (m *Monitor) Subscribe(optLabel ...string) Subscription

func (*Monitor) UnsubscribeAll added in v1.21.6

func (m *Monitor) UnsubscribeAll(err error)

type Options

type Options struct {
	// Logger used by ethmonitor to log warnings and debug info
	Logger *slog.Logger

	// (optional) ChainID is the chainID to use for the monitor. We
	// also confirm it with the provider, but in case you're using a monitor
	// with a faulty node, this can be used to manually set the chainID.
	ChainID *big.Int

	// PollingInterval to query the chain for new blocks
	PollingInterval time.Duration

	// StreamingErrorResetInterval is the time to reset the streaming error count
	StreamingErrorResetInterval time.Duration

	// StreamingRetryAfter is the time to wait before retrying the streaming again
	StreamingRetryAfter time.Duration

	// StreamingErrNumToSwitchToPolling is the number of errors before switching to polling
	StreamingErrNumToSwitchToPolling int

	// StreamingDisabled flag to force disable streaming even if the provider supports it
	StreamingDisabled bool

	// Auto-unsubscribe on monitor stop or error
	UnsubscribeOnStop bool

	// Timeout duration used by the rpc client when fetching data from the remote node.
	Timeout time.Duration

	// StartBlockNumber to begin the monitor from.
	StartBlockNumber *big.Int

	// Bootstrap flag which indicates the monitor will expect the monitor's
	// events to be bootstrapped, and will continue from that point. This also
	// takes precedence over StartBlockNumber when set to true.
	Bootstrap bool

	// TrailNumBlocksBehindHead is the number of blocks we trail behind
	// the head of the chain before broadcasting new events to the subscribers.
	TrailNumBlocksBehindHead int

	// BlockRetentionLimit is the number of blocks we keep on the canonical chain
	// cache.
	BlockRetentionLimit int

	// WithLogs will include logs with the blocks if specified true.
	WithLogs bool

	// LogTopics selects values for the first topic of each log. Empty filtered
	// results are accepted because a nonzero bloom does not prove a match.
	LogTopics []common.Hash

	// CacheBackend to use for caching block data
	// NOTE: do not use this unless you know what you're doing.
	// In most cases leave this nil.
	CacheBackend cachestore.Backend

	// CacheExpiry is how long to keep each record in cache
	CacheExpiry time.Duration

	// PrefetchConcurrency is the number of workers fetching blocks (and their
	// logs, when WithLogs is set) ahead of the monitor while it trails the chain
	// head. Prefetched payloads land in the cache, so the monitor's serial loop
	// reads them as cache hits instead of paying node round-trips per block.
	// Useful on chains whose block rate outpaces a serial fetch. If no
	// CacheBackend is set, an in-memory cache is used. 0 disables prefetching.
	// A prefetched block may be reorged out before it is read, producing an
	// Added then Removed event; subscribers must already handle such reorgs.
	PrefetchConcurrency int

	// PrefetchWindow is how many blocks past the monitor's next block the
	// prefetcher may fetch. Defaults to 4x PrefetchConcurrency.
	PrefetchWindow int

	// Alerter config via github.com/goware/alerter
	Alerter util.Alerter

	// MaxLogsAttempts is the number of failed eth_getLogs attempts for a block,
	// after which its logs are taken from its receipts. Defaults to 10.
	MaxLogsAttempts int

	// StrictLogs, when true, never publishes a block whose logs can't be
	// confirmed by eth_getLogs or its receipts. The monitor keeps retrying and
	// returns ErrFatal(ErrQueueFull) if the publish queue overflows, which was
	// the behaviour before this option existed. When false (the default), such
	// a block is published with empty logs once it leaves the chain cache
	// (BlockRetentionLimit), with an Error log and an alert.
	StrictLogs bool

	// DebugLogging toggle
	DebugLogging bool
}

type Subscription

type Subscription interface {
	Blocks() <-chan Blocks
	Done() <-chan struct{}
	Err() error
	Unsubscribe()
}

Directories

Path Synopsis
internal
mocks
Package mocks is a generated GoMock package.
Package mocks is a generated GoMock package.

Jump to

Keyboard shortcuts

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