Ethereum compatible actor event API
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/filecoin-project/lotus/chain/beacon"
|
||||
"github.com/filecoin-project/lotus/chain/consensus"
|
||||
"github.com/filecoin-project/lotus/chain/consensus/filcns"
|
||||
"github.com/filecoin-project/lotus/chain/events"
|
||||
"github.com/filecoin-project/lotus/chain/exchange"
|
||||
"github.com/filecoin-project/lotus/chain/gen/slashfilter"
|
||||
"github.com/filecoin-project/lotus/chain/market"
|
||||
@@ -156,6 +157,7 @@ var ChainNode = Options(
|
||||
Override(new(messagesigner.MpoolNonceAPI), From(new(*messagepool.MessagePool))),
|
||||
Override(new(full.ChainModuleAPI), From(new(full.ChainModule))),
|
||||
Override(new(full.EthModuleAPI), From(new(full.EthModule))),
|
||||
Override(new(full.EthEventAPI), From(new(full.EthEvent))),
|
||||
Override(new(full.GasModuleAPI), From(new(full.GasModule))),
|
||||
Override(new(full.MpoolModuleAPI), From(new(full.MpoolModule))),
|
||||
Override(new(full.StateModuleAPI), From(new(full.StateModule))),
|
||||
@@ -239,6 +241,10 @@ func ConfigFullNode(c interface{}) Option {
|
||||
Unset(new(*wallet.LocalWallet)),
|
||||
Override(new(wallet.Default), wallet.NilDefault),
|
||||
),
|
||||
|
||||
// Actor event filtering support
|
||||
Override(new(events.EventAPI), From(new(modules.EventAPI))),
|
||||
Override(new(full.EthEventAPI), modules.EthEvent(cfg.ActorEvent)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+16
-5
@@ -71,11 +71,12 @@ func defCommon() Common {
|
||||
DirectPeers: nil,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var DefaultDefaultMaxFee = types.MustParseFIL("0.07")
|
||||
var DefaultSimultaneousTransfers = uint64(20)
|
||||
var (
|
||||
DefaultDefaultMaxFee = types.MustParseFIL("0.07")
|
||||
DefaultSimultaneousTransfers = uint64(20)
|
||||
)
|
||||
|
||||
// DefaultFullNode returns the default config
|
||||
func DefaultFullNode() *FullNode {
|
||||
@@ -98,6 +99,14 @@ func DefaultFullNode() *FullNode {
|
||||
HotStoreFullGCFrequency: 20,
|
||||
},
|
||||
},
|
||||
ActorEvent: ActorEventConfig{
|
||||
EnableRealTimeFilterAPI: false,
|
||||
EnableHistoricFilterAPI: false,
|
||||
FilterTTL: Duration(time.Hour * 24),
|
||||
MaxFilters: 100,
|
||||
MaxFilterResults: 10000,
|
||||
MaxFilterHeightRange: 2880, // conservative limit of one day
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +262,10 @@ func DefaultStorageMiner() *StorageMiner {
|
||||
return cfg
|
||||
}
|
||||
|
||||
var _ encoding.TextMarshaler = (*Duration)(nil)
|
||||
var _ encoding.TextUnmarshaler = (*Duration)(nil)
|
||||
var (
|
||||
_ encoding.TextMarshaler = (*Duration)(nil)
|
||||
_ encoding.TextUnmarshaler = (*Duration)(nil)
|
||||
)
|
||||
|
||||
// Duration is a wrapper type for time.Duration
|
||||
// for decoding and encoding from/to TOML
|
||||
|
||||
+29
-1
@@ -27,6 +27,7 @@ type FullNode struct {
|
||||
Wallet Wallet
|
||||
Fees FeeConfig
|
||||
Chainstore Chainstore
|
||||
ActorEvent ActorEventConfig
|
||||
}
|
||||
|
||||
// // Common
|
||||
@@ -168,7 +169,6 @@ type DealmakingConfig struct {
|
||||
}
|
||||
|
||||
type IndexProviderConfig struct {
|
||||
|
||||
// Enable set whether to enable indexing announcement to the network and expose endpoints that
|
||||
// allow indexer nodes to process announcements. Enabled by default.
|
||||
Enable bool
|
||||
@@ -601,3 +601,31 @@ type Wallet struct {
|
||||
type FeeConfig struct {
|
||||
DefaultMaxFee types.FIL
|
||||
}
|
||||
|
||||
type ActorEventConfig struct {
|
||||
// EnableRealTimeFilterAPI enables APIs that can create and query filters for actor events as they are emitted.
|
||||
EnableRealTimeFilterAPI bool
|
||||
|
||||
// EnableHistoricFilterAPI enables APIs that can create and query filters for actor events that occurred in the past.
|
||||
// A queryable index of events will be maintained.
|
||||
EnableHistoricFilterAPI bool
|
||||
|
||||
// FilterTTL specifies the time to live for actor event filters. Filters that haven't been accessed longer than
|
||||
// this time become eligible for automatic deletion.
|
||||
FilterTTL Duration
|
||||
|
||||
// MaxFilters specifies the maximum number of filters that may exist at any one time.
|
||||
MaxFilters int
|
||||
|
||||
// MaxFilterResults specifies the maximum number of results that can be accumulated by an actor event filter.
|
||||
MaxFilterResults int
|
||||
|
||||
// MaxFilterHeightRange specifies the maximum range of heights that can be used in a filter (to avoid querying
|
||||
// the entire chain)
|
||||
MaxFilterHeightRange uint64
|
||||
|
||||
// Others, not implemented yet:
|
||||
// Set a limit on the number of active websocket subscriptions (may be zero)
|
||||
// Set a timeout for subscription clients
|
||||
// Set upper bound on index size
|
||||
}
|
||||
|
||||
+587
-3
@@ -3,9 +3,13 @@ package full
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ipfs/go-cid"
|
||||
cbg "github.com/whyrusleeping/cbor-gen"
|
||||
"go.uber.org/fx"
|
||||
@@ -17,11 +21,12 @@ import (
|
||||
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
||||
"github.com/filecoin-project/go-state-types/builtin/v10/eam"
|
||||
"github.com/filecoin-project/go-state-types/builtin/v10/evm"
|
||||
"github.com/filecoin-project/specs-actors/actors/builtin"
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
"github.com/filecoin-project/lotus/chain/actors"
|
||||
builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin"
|
||||
"github.com/filecoin-project/lotus/chain/events/filter"
|
||||
"github.com/filecoin-project/lotus/chain/messagepool"
|
||||
"github.com/filecoin-project/lotus/chain/stmgr"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
@@ -57,7 +62,22 @@ type EthModuleAPI interface {
|
||||
// EthFeeHistory(ctx context.Context, blkCount string)
|
||||
}
|
||||
|
||||
var _ EthModuleAPI = *new(api.FullNode)
|
||||
type EthEventAPI interface {
|
||||
EthGetLogs(ctx context.Context, filter *api.EthFilterSpec) (*api.EthFilterResult, error)
|
||||
EthGetFilterChanges(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error)
|
||||
EthGetFilterLogs(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error)
|
||||
EthNewFilter(ctx context.Context, filter *api.EthFilterSpec) (api.EthFilterID, error)
|
||||
EthNewBlockFilter(ctx context.Context) (api.EthFilterID, error)
|
||||
EthNewPendingTransactionFilter(ctx context.Context) (api.EthFilterID, error)
|
||||
EthUninstallFilter(ctx context.Context, id api.EthFilterID) (bool, error)
|
||||
EthSubscribe(ctx context.Context, eventTypes []string, params api.EthSubscriptionParams) (<-chan api.EthSubscriptionResponse, error)
|
||||
EthUnsubscribe(ctx context.Context, id api.EthSubscriptionID) (bool, error)
|
||||
}
|
||||
|
||||
var (
|
||||
_ EthModuleAPI = *new(api.FullNode)
|
||||
_ EthEventAPI = *new(api.FullNode)
|
||||
)
|
||||
|
||||
// EthModule provides a default implementation of EthModuleAPI.
|
||||
// It can be swapped out with another implementation through Dependency
|
||||
@@ -76,12 +96,25 @@ type EthModule struct {
|
||||
|
||||
var _ EthModuleAPI = (*EthModule)(nil)
|
||||
|
||||
type EthEvent struct {
|
||||
Chain *store.ChainStore
|
||||
EventFilterManager *filter.EventFilterManager
|
||||
TipSetFilterManager *filter.TipSetFilterManager
|
||||
MemPoolFilterManager *filter.MemPoolFilterManager
|
||||
FilterStore filter.FilterStore
|
||||
SubManager ethSubscriptionManager
|
||||
MaxFilterHeightRange abi.ChainEpoch
|
||||
}
|
||||
|
||||
var _ EthEventAPI = (*EthEvent)(nil)
|
||||
|
||||
type EthAPI struct {
|
||||
fx.In
|
||||
|
||||
Chain *store.ChainStore
|
||||
|
||||
EthModuleAPI
|
||||
EthEventAPI
|
||||
}
|
||||
|
||||
func (a *EthModule) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) {
|
||||
@@ -463,7 +496,7 @@ func (a *EthModule) EthProtocolVersion(ctx context.Context) (api.EthUint64, erro
|
||||
}
|
||||
|
||||
func (a *EthModule) EthMaxPriorityFeePerGas(ctx context.Context) (api.EthBigInt, error) {
|
||||
gasPremium, err := a.GasAPI.GasEstimateGasPremium(ctx, 0, builtin.SystemActorAddr, 10000, types.EmptyTSK)
|
||||
gasPremium, err := a.GasAPI.GasEstimateGasPremium(ctx, 0, builtinactors.SystemActorAddr, 10000, types.EmptyTSK)
|
||||
if err != nil {
|
||||
return api.EthBigInt(big.Zero()), err
|
||||
}
|
||||
@@ -826,3 +859,554 @@ func (a *EthModule) ethTxFromFilecoinMessageLookup(ctx context.Context, msgLooku
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthGetLogs(ctx context.Context, filter *api.EthFilterSpec) (*api.EthFilterResult, error) {
|
||||
// TODO: implement EthGetLogs
|
||||
return nil, api.ErrNotSupported
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthGetFilterChanges(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error) {
|
||||
if e.FilterStore == nil {
|
||||
return nil, api.ErrNotSupported
|
||||
}
|
||||
|
||||
f, err := e.FilterStore.Get(ctx, string(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch fc := f.(type) {
|
||||
case filterEventCollector:
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
||||
case filterTipSetCollector:
|
||||
return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx))
|
||||
case filterMessageCollector:
|
||||
return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx))
|
||||
}
|
||||
|
||||
return nil, xerrors.Errorf("unknown filter type")
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthGetFilterLogs(ctx context.Context, id api.EthFilterID) (*api.EthFilterResult, error) {
|
||||
if e.FilterStore == nil {
|
||||
return nil, api.ErrNotSupported
|
||||
}
|
||||
|
||||
f, err := e.FilterStore.Get(ctx, string(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch fc := f.(type) {
|
||||
case filterEventCollector:
|
||||
return ethFilterResultFromEvents(fc.TakeCollectedEvents(ctx))
|
||||
}
|
||||
|
||||
return nil, xerrors.Errorf("wrong filter type")
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthNewFilter(ctx context.Context, filter *api.EthFilterSpec) (api.EthFilterID, error) {
|
||||
if e.FilterStore == nil || e.EventFilterManager == nil {
|
||||
return "", api.ErrNotSupported
|
||||
}
|
||||
|
||||
var (
|
||||
minHeight abi.ChainEpoch
|
||||
maxHeight abi.ChainEpoch
|
||||
tipsetCid cid.Cid
|
||||
addresses []address.Address
|
||||
keys = map[string][][]byte{}
|
||||
)
|
||||
|
||||
if filter.BlockHash != nil {
|
||||
if filter.FromBlock != nil || filter.ToBlock != nil {
|
||||
return "", xerrors.Errorf("must not specify block hash and from/to block")
|
||||
}
|
||||
|
||||
// TODO: derive a tipset hash from eth hash - might need to push this down into the EventFilterManager
|
||||
} else {
|
||||
if filter.FromBlock == nil || *filter.FromBlock == "latest" {
|
||||
ts := e.Chain.GetHeaviestTipSet()
|
||||
minHeight = ts.Height()
|
||||
} else if *filter.FromBlock == "earliest" {
|
||||
minHeight = 0
|
||||
} else if *filter.FromBlock == "pending" {
|
||||
return "", api.ErrNotSupported
|
||||
} else {
|
||||
epoch, err := strconv.ParseUint(*filter.FromBlock, 10, 64)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("invalid epoch")
|
||||
}
|
||||
minHeight = abi.ChainEpoch(epoch)
|
||||
}
|
||||
|
||||
if filter.ToBlock == nil || *filter.ToBlock == "latest" {
|
||||
// here latest means the latest at the time
|
||||
maxHeight = -1
|
||||
} else if *filter.ToBlock == "earliest" {
|
||||
maxHeight = 0
|
||||
} else if *filter.ToBlock == "pending" {
|
||||
return "", api.ErrNotSupported
|
||||
} else {
|
||||
epoch, err := strconv.ParseUint(*filter.ToBlock, 10, 64)
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("invalid epoch")
|
||||
}
|
||||
maxHeight = abi.ChainEpoch(epoch)
|
||||
}
|
||||
|
||||
// Validate height ranges are within limits set by node operator
|
||||
if minHeight == -1 && maxHeight > 0 {
|
||||
// Here the client is looking for events between the head and some future height
|
||||
ts := e.Chain.GetHeaviestTipSet()
|
||||
if maxHeight-ts.Height() > e.MaxFilterHeightRange {
|
||||
return "", xerrors.Errorf("invalid epoch range")
|
||||
}
|
||||
} else if minHeight >= 0 && maxHeight == -1 {
|
||||
// Here the client is looking for events between some time in the past and the current head
|
||||
ts := e.Chain.GetHeaviestTipSet()
|
||||
if ts.Height()-minHeight > e.MaxFilterHeightRange {
|
||||
return "", xerrors.Errorf("invalid epoch range")
|
||||
}
|
||||
|
||||
} else if minHeight >= 0 && maxHeight >= 0 {
|
||||
if minHeight > maxHeight || maxHeight-minHeight > e.MaxFilterHeightRange {
|
||||
return "", xerrors.Errorf("invalid epoch range")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
for _, ea := range filter.Address {
|
||||
a, err := ea.ToFilecoinAddress()
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("invalid address %x", ea)
|
||||
}
|
||||
addresses = append(addresses, a)
|
||||
}
|
||||
|
||||
for idx, vals := range filter.Topics {
|
||||
// Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4
|
||||
key := fmt.Sprintf("topic%d", idx+1)
|
||||
keyvals := make([][]byte, len(vals))
|
||||
for i, v := range vals {
|
||||
keyvals[i] = v[:]
|
||||
}
|
||||
keys[key] = keyvals
|
||||
}
|
||||
|
||||
f, err := e.EventFilterManager.Install(ctx, minHeight, maxHeight, tipsetCid, addresses, keys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := e.FilterStore.Add(ctx, f); err != nil {
|
||||
// Could not record in store, attempt to delete filter to clean up
|
||||
err2 := e.TipSetFilterManager.Remove(ctx, f.ID())
|
||||
if err2 != nil {
|
||||
return "", xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return api.EthFilterID(f.ID()), nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthNewBlockFilter(ctx context.Context) (api.EthFilterID, error) {
|
||||
if e.FilterStore == nil || e.TipSetFilterManager == nil {
|
||||
return "", api.ErrNotSupported
|
||||
}
|
||||
|
||||
f, err := e.TipSetFilterManager.Install(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := e.FilterStore.Add(ctx, f); err != nil {
|
||||
// Could not record in store, attempt to delete filter to clean up
|
||||
err2 := e.TipSetFilterManager.Remove(ctx, f.ID())
|
||||
if err2 != nil {
|
||||
return "", xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return api.EthFilterID(f.ID()), nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthNewPendingTransactionFilter(ctx context.Context) (api.EthFilterID, error) {
|
||||
if e.FilterStore == nil || e.MemPoolFilterManager == nil {
|
||||
return "", api.ErrNotSupported
|
||||
}
|
||||
|
||||
f, err := e.MemPoolFilterManager.Install(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := e.FilterStore.Add(ctx, f); err != nil {
|
||||
// Could not record in store, attempt to delete filter to clean up
|
||||
err2 := e.MemPoolFilterManager.Remove(ctx, f.ID())
|
||||
if err2 != nil {
|
||||
return "", xerrors.Errorf("encountered error %v while removing new filter due to %v", err2, err)
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
|
||||
return api.EthFilterID(f.ID()), nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthUninstallFilter(ctx context.Context, id api.EthFilterID) (bool, error) {
|
||||
if e.FilterStore == nil {
|
||||
return false, api.ErrNotSupported
|
||||
}
|
||||
|
||||
f, err := e.FilterStore.Get(ctx, string(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, filter.ErrFilterNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := e.uninstallFilter(ctx, f); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) uninstallFilter(ctx context.Context, f filter.Filter) error {
|
||||
switch f.(type) {
|
||||
case *filter.EventFilter:
|
||||
err := e.EventFilterManager.Remove(ctx, f.ID())
|
||||
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
||||
return err
|
||||
}
|
||||
case *filter.TipSetFilter:
|
||||
err := e.TipSetFilterManager.Remove(ctx, f.ID())
|
||||
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
||||
return err
|
||||
}
|
||||
case *filter.MemPoolFilter:
|
||||
err := e.MemPoolFilterManager.Remove(ctx, f.ID())
|
||||
if err != nil && !errors.Is(err, filter.ErrFilterNotFound) {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return xerrors.Errorf("unknown filter type")
|
||||
}
|
||||
|
||||
return e.FilterStore.Remove(ctx, f.ID())
|
||||
}
|
||||
|
||||
const (
|
||||
EthSubscribeEventTypeHeads = "newHeads"
|
||||
EthSubscribeEventTypeLogs = "logs"
|
||||
)
|
||||
|
||||
func (e *EthEvent) EthSubscribe(ctx context.Context, eventTypes []string, params api.EthSubscriptionParams) (<-chan api.EthSubscriptionResponse, error) {
|
||||
// Note that go-jsonrpc will set the method field of the response to "xrpc.ch.val" but the ethereum api expects the name of the
|
||||
// method to be "eth_subscription". This probably doesn't matter in practice.
|
||||
|
||||
// Validate event types and parameters first
|
||||
for _, et := range eventTypes {
|
||||
switch et {
|
||||
case EthSubscribeEventTypeHeads:
|
||||
case EthSubscribeEventTypeLogs:
|
||||
default:
|
||||
return nil, xerrors.Errorf("unsupported event type: %s", et)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := e.SubManager.StartSubscription(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, et := range eventTypes {
|
||||
switch et {
|
||||
case EthSubscribeEventTypeHeads:
|
||||
f, err := e.TipSetFilterManager.Install(ctx)
|
||||
if err != nil {
|
||||
// clean up any previous filters added and stop the sub
|
||||
_, _ = e.EthUnsubscribe(ctx, api.EthSubscriptionID(sub.id))
|
||||
return nil, err
|
||||
}
|
||||
sub.addFilter(ctx, f)
|
||||
|
||||
case EthSubscribeEventTypeLogs:
|
||||
keys := map[string][][]byte{}
|
||||
for idx, vals := range params.Topics {
|
||||
// Ethereum topics are emitted using `LOG{0..4}` opcodes resulting in topics1..4
|
||||
key := fmt.Sprintf("topic%d", idx+1)
|
||||
keyvals := make([][]byte, len(vals))
|
||||
for i, v := range vals {
|
||||
keyvals[i] = v[:]
|
||||
}
|
||||
keys[key] = keyvals
|
||||
}
|
||||
|
||||
f, err := e.EventFilterManager.Install(ctx, -1, -1, cid.Undef, []address.Address{}, keys)
|
||||
if err != nil {
|
||||
// clean up any previous filters added and stop the sub
|
||||
_, _ = e.EthUnsubscribe(ctx, api.EthSubscriptionID(sub.id))
|
||||
return nil, err
|
||||
}
|
||||
sub.addFilter(ctx, f)
|
||||
}
|
||||
}
|
||||
|
||||
return sub.out, nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) EthUnsubscribe(ctx context.Context, id api.EthSubscriptionID) (bool, error) {
|
||||
filters, err := e.SubManager.StopSubscription(ctx, string(id))
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
for _, f := range filters {
|
||||
if err := e.uninstallFilter(ctx, f); err != nil {
|
||||
// this will leave the filter a zombie, collecting events up to the maximum allowed
|
||||
log.Warnf("failed to remove filter when unsubscribing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// GC runs a garbage collection loop, deleting filters that have not been used within the ttl window
|
||||
func (e *EthEvent) GC(ctx context.Context, ttl time.Duration) {
|
||||
if e.FilterStore == nil {
|
||||
return
|
||||
}
|
||||
|
||||
tt := time.NewTicker(time.Minute * 30)
|
||||
defer tt.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tt.C:
|
||||
fs := e.FilterStore.NotTakenSince(time.Now().Add(-ttl))
|
||||
for _, f := range fs {
|
||||
if err := e.uninstallFilter(ctx, f); err != nil {
|
||||
log.Warnf("Failed to remove actor event filter during garbage collection: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type filterEventCollector interface {
|
||||
TakeCollectedEvents(context.Context) []*filter.CollectedEvent
|
||||
}
|
||||
|
||||
type filterMessageCollector interface {
|
||||
TakeCollectedMessages(context.Context) []cid.Cid
|
||||
}
|
||||
|
||||
type filterTipSetCollector interface {
|
||||
TakeCollectedTipSets(context.Context) []types.TipSetKey
|
||||
}
|
||||
|
||||
var (
|
||||
ethTopic1 = []byte("topic1")
|
||||
ethTopic2 = []byte("topic2")
|
||||
ethTopic3 = []byte("topic3")
|
||||
ethTopic4 = []byte("topic4")
|
||||
)
|
||||
|
||||
func ethFilterResultFromEvents(evs []*filter.CollectedEvent) (*api.EthFilterResult, error) {
|
||||
res := &api.EthFilterResult{}
|
||||
|
||||
for _, ev := range evs {
|
||||
log := api.EthLog{
|
||||
Removed: ev.Reverted,
|
||||
LogIndex: api.EthUint64(ev.EventIdx),
|
||||
TransactionIndex: api.EthUint64(ev.MsgIdx),
|
||||
BlockNumber: api.EthUint64(ev.Height),
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
for _, entry := range ev.Event.Entries {
|
||||
hash := api.EthHashData(entry.Value)
|
||||
if bytes.Equal(entry.Key, ethTopic1) || bytes.Equal(entry.Key, ethTopic2) || bytes.Equal(entry.Key, ethTopic3) || bytes.Equal(entry.Key, ethTopic4) {
|
||||
log.Topics = append(log.Topics, hash)
|
||||
} else {
|
||||
log.Data = append(log.Data, hash)
|
||||
}
|
||||
}
|
||||
|
||||
log.Address, err = api.EthAddressFromFilecoinAddress(ev.Event.Emitter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.TransactionHash, err = api.EthHashFromCid(ev.MsgCid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err := ev.TipSetKey.Cid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.BlockHash, err = api.EthHashFromCid(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.NewLogs = append(res.NewLogs, log)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*api.EthFilterResult, error) {
|
||||
res := &api.EthFilterResult{}
|
||||
|
||||
for _, tsk := range tsks {
|
||||
c, err := tsk.Cid()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash, err := api.EthHashFromCid(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.NewBlockHashes = append(res.NewBlockHashes, hash)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ethFilterResultFromMessages(cs []cid.Cid) (*api.EthFilterResult, error) {
|
||||
res := &api.EthFilterResult{}
|
||||
|
||||
for _, c := range cs {
|
||||
hash, err := api.EthHashFromCid(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.NewTransactionHashes = append(res.NewTransactionHashes, hash)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
type ethSubscriptionManager struct {
|
||||
mu sync.Mutex
|
||||
subs map[string]*ethSubscription
|
||||
}
|
||||
|
||||
func (e *ethSubscriptionManager) StartSubscription(ctx context.Context) (*ethSubscription, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("new uuid: %w", err)
|
||||
}
|
||||
|
||||
ctx, quit := context.WithCancel(ctx)
|
||||
|
||||
sub := ðSubscription{
|
||||
id: id.String(),
|
||||
in: make(chan interface{}, 200),
|
||||
out: make(chan api.EthSubscriptionResponse),
|
||||
quit: quit,
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
if e.subs == nil {
|
||||
e.subs = make(map[string]*ethSubscription)
|
||||
}
|
||||
e.subs[sub.id] = sub
|
||||
e.mu.Unlock()
|
||||
|
||||
go sub.start(ctx)
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (e *ethSubscriptionManager) StopSubscription(ctx context.Context, id string) ([]filter.Filter, error) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
sub, ok := e.subs[id]
|
||||
if !ok {
|
||||
return nil, xerrors.Errorf("subscription not found")
|
||||
}
|
||||
sub.stop()
|
||||
delete(e.subs, id)
|
||||
|
||||
return sub.filters, nil
|
||||
}
|
||||
|
||||
type ethSubscription struct {
|
||||
id string
|
||||
in chan interface{}
|
||||
out chan api.EthSubscriptionResponse
|
||||
|
||||
mu sync.Mutex
|
||||
filters []filter.Filter
|
||||
quit func()
|
||||
}
|
||||
|
||||
func (e *ethSubscription) addFilter(ctx context.Context, f filter.Filter) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
f.SetSubChannel(e.in)
|
||||
e.filters = append(e.filters, f)
|
||||
}
|
||||
|
||||
func (e *ethSubscription) start(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case v := <-e.in:
|
||||
resp := api.EthSubscriptionResponse{
|
||||
SubscriptionID: api.EthSubscriptionID(e.id),
|
||||
}
|
||||
|
||||
var err error
|
||||
switch vt := v.(type) {
|
||||
case *filter.CollectedEvent:
|
||||
resp.Result, err = ethFilterResultFromEvents([]*filter.CollectedEvent{vt})
|
||||
case *types.TipSet:
|
||||
resp.Result = vt
|
||||
default:
|
||||
log.Warnf("unexpected subscription value type: %T", vt)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case e.out <- resp:
|
||||
default:
|
||||
// Skip if client is not reading responses
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ethSubscription) stop() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
if e.quit != nil {
|
||||
e.quit()
|
||||
close(e.out)
|
||||
e.quit = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/filecoin-project/go-state-types/abi"
|
||||
|
||||
"github.com/filecoin-project/lotus/chain/events"
|
||||
"github.com/filecoin-project/lotus/chain/events/filter"
|
||||
"github.com/filecoin-project/lotus/chain/messagepool"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
"github.com/filecoin-project/lotus/node/config"
|
||||
"github.com/filecoin-project/lotus/node/impl/full"
|
||||
"github.com/filecoin-project/lotus/node/modules/helpers"
|
||||
)
|
||||
|
||||
type EventAPI struct {
|
||||
fx.In
|
||||
|
||||
full.ChainAPI
|
||||
full.StateAPI
|
||||
}
|
||||
|
||||
var _ events.EventAPI = &EventAPI{}
|
||||
|
||||
func EthEvent(cfg config.ActorEventConfig) func(helpers.MetricsCtx, fx.Lifecycle, *store.ChainStore, EventAPI, *messagepool.MessagePool) (*full.EthEvent, error) {
|
||||
return func(mctx helpers.MetricsCtx, lc fx.Lifecycle, cs *store.ChainStore, evapi EventAPI, mp *messagepool.MessagePool) (*full.EthEvent, error) {
|
||||
ee := &full.EthEvent{
|
||||
Chain: cs,
|
||||
MaxFilterHeightRange: abi.ChainEpoch(cfg.MaxFilterHeightRange),
|
||||
}
|
||||
|
||||
if !cfg.EnableRealTimeFilterAPI && !cfg.EnableHistoricFilterAPI {
|
||||
// all event functionality is disabled
|
||||
return ee, nil
|
||||
}
|
||||
|
||||
ee.FilterStore = filter.NewMemFilterStore(cfg.MaxFilters)
|
||||
|
||||
// Start garbage collection for filters
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
go ee.GC(ctx, time.Duration(cfg.FilterTTL))
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if cfg.EnableRealTimeFilterAPI {
|
||||
ee.EventFilterManager = &filter.EventFilterManager{
|
||||
ChainStore: cs,
|
||||
MaxFilterResults: cfg.MaxFilterResults,
|
||||
}
|
||||
ee.TipSetFilterManager = &filter.TipSetFilterManager{
|
||||
MaxFilterResults: cfg.MaxFilterResults,
|
||||
}
|
||||
ee.MemPoolFilterManager = &filter.MemPoolFilterManager{
|
||||
MaxFilterResults: cfg.MaxFilterResults,
|
||||
}
|
||||
|
||||
const ChainHeadConfidence = 1
|
||||
|
||||
ctx := helpers.LifecycleCtx(mctx, lc)
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(context.Context) error {
|
||||
ev, err := events.NewEventsWithConfidence(ctx, &evapi, ChainHeadConfidence)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// ignore returned tipsets
|
||||
_ = ev.Observe(ee.EventFilterManager)
|
||||
_ = ev.Observe(ee.TipSetFilterManager)
|
||||
|
||||
ch, err := mp.Updates(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go ee.MemPoolFilterManager.WaitForMpoolUpdates(ctx, ch)
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if cfg.EnableHistoricFilterAPI {
|
||||
// TODO: enable indexer
|
||||
}
|
||||
|
||||
return ee, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user