Tests for builtin actor events API
This commit is contained in:
@@ -362,9 +362,8 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
|
||||
Name: "DisableRealTimeFilterAPI",
|
||||
Type: "bool",
|
||||
|
||||
Comment: `EnableEthRPC enables APIs that
|
||||
DisableRealTimeFilterAPI will disable the RealTimeFilterAPI that can create and query filters for actor events as they are emitted.
|
||||
The API is enabled when EnableEthRPC is true, but can be disabled selectively with this flag.`,
|
||||
Comment: `DisableRealTimeFilterAPI will disable the RealTimeFilterAPI that can create and query filters for actor events as they are emitted.
|
||||
The API is enabled when EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
|
||||
},
|
||||
{
|
||||
Name: "DisableHistoricFilterAPI",
|
||||
@@ -372,7 +371,7 @@ The API is enabled when EnableEthRPC is true, but can be disabled selectively wi
|
||||
|
||||
Comment: `DisableHistoricFilterAPI will disable the HistoricFilterAPI that can create and query filters for actor events
|
||||
that occurred in the past. HistoricFilterAPI maintains a queryable index of events.
|
||||
The API is enabled when EnableEthRPC is true, but can be disabled selectively with this flag.`,
|
||||
The API is enabled when EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
|
||||
},
|
||||
{
|
||||
Name: "FilterTTL",
|
||||
@@ -459,7 +458,8 @@ This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, bu
|
||||
Name: "EnableActorEventsAPI",
|
||||
Type: "bool",
|
||||
|
||||
Comment: ``,
|
||||
Comment: `EnableActorEventsAPI enables the Actor events API that enables clients to consume events emitted by (smart contracts + built-in Actors).
|
||||
This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be disabled by config options above.`,
|
||||
},
|
||||
{
|
||||
Name: "EthTxHashMappingLifetimeDays",
|
||||
|
||||
@@ -786,6 +786,8 @@ type FevmConfig struct {
|
||||
// This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be disabled by config options above.
|
||||
EnableEthRPC bool
|
||||
|
||||
// EnableActorEventsAPI enables the Actor events API that enables clients to consume events emitted by (smart contracts + built-in Actors).
|
||||
// This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be disabled by config options above.
|
||||
EnableActorEventsAPI bool
|
||||
|
||||
// EthTxHashMappingLifetimeDays the transaction hash lookup database will delete mappings that have been stored for more than x days
|
||||
@@ -796,14 +798,13 @@ type FevmConfig struct {
|
||||
}
|
||||
|
||||
type Events struct {
|
||||
// EnableEthRPC enables APIs that
|
||||
// DisableRealTimeFilterAPI will disable the RealTimeFilterAPI that can create and query filters for actor events as they are emitted.
|
||||
// The API is enabled when EnableEthRPC is true, but can be disabled selectively with this flag.
|
||||
// The API is enabled when EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.
|
||||
DisableRealTimeFilterAPI bool
|
||||
|
||||
// DisableHistoricFilterAPI will disable the HistoricFilterAPI that can create and query filters for actor events
|
||||
// that occurred in the past. HistoricFilterAPI maintains a queryable index of events.
|
||||
// The API is enabled when EnableEthRPC is true, but can be disabled selectively with this flag.
|
||||
// The API is enabled when EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.
|
||||
DisableHistoricFilterAPI bool
|
||||
|
||||
// FilterTTL specifies the time to live for actor event filters. Filters that haven't been accessed longer than
|
||||
|
||||
@@ -3,6 +3,7 @@ package full
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
"go.uber.org/fx"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/chain/events/filter"
|
||||
"github.com/filecoin-project/lotus/chain/store"
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
)
|
||||
|
||||
@@ -27,6 +29,7 @@ var (
|
||||
type ActorEvent struct {
|
||||
EventFilterManager *filter.EventFilterManager
|
||||
MaxFilterHeightRange abi.ChainEpoch
|
||||
Chain *store.ChainStore
|
||||
}
|
||||
|
||||
var _ ActorEventAPI = (*ActorEvent)(nil)
|
||||
@@ -41,8 +44,13 @@ func (a *ActorEvent) GetActorEvents(ctx context.Context, filter *types.ActorEven
|
||||
return nil, api.ErrNotSupported
|
||||
}
|
||||
|
||||
params, err := a.parseFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a temporary filter
|
||||
f, err := a.EventFilterManager.Install(ctx, filter.MinEpoch, filter.MaxEpoch, cid.Undef, filter.Addresses, filter.Fields, false)
|
||||
f, err := a.EventFilterManager.Install(ctx, params.MinHeight, params.MaxHeight, params.TipSetCid, filter.Addresses, filter.Fields, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -52,11 +60,58 @@ func (a *ActorEvent) GetActorEvents(ctx context.Context, filter *types.ActorEven
|
||||
return evs, err
|
||||
}
|
||||
|
||||
type filterParams struct {
|
||||
MinHeight abi.ChainEpoch
|
||||
MaxHeight abi.ChainEpoch
|
||||
TipSetCid cid.Cid
|
||||
}
|
||||
|
||||
func (a *ActorEvent) parseFilter(f *types.ActorEventFilter) (*filterParams, error) {
|
||||
if f.TipSetCid != nil {
|
||||
if len(f.FromEpoch) != 0 || len(f.ToEpoch) != 0 {
|
||||
return nil, fmt.Errorf("cannot specify both TipSetCid and FromEpoch/ToEpoch")
|
||||
}
|
||||
|
||||
return &filterParams{
|
||||
MinHeight: 0,
|
||||
MaxHeight: 0,
|
||||
TipSetCid: *f.TipSetCid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
from := f.FromEpoch
|
||||
if len(from) != 0 && from != "latest" && from != "earliest" && !strings.HasPrefix(from, "0x") {
|
||||
from = "0x" + from
|
||||
}
|
||||
|
||||
to := f.ToEpoch
|
||||
if len(to) != 0 && to != "latest" && to != "earliest" && !strings.HasPrefix(to, "0x") {
|
||||
to = "0x" + to
|
||||
}
|
||||
|
||||
min, max, err := parseBlockRange(a.Chain.GetHeaviestTipSet().Height(), &from, &to, a.MaxFilterHeightRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &filterParams{
|
||||
MinHeight: min,
|
||||
MaxHeight: max,
|
||||
TipSetCid: cid.Undef,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ActorEvent) SubscribeActorEvents(ctx context.Context, f *types.SubActorEventFilter) (<-chan *types.ActorEvent, error) {
|
||||
if a.EventFilterManager == nil {
|
||||
return nil, api.ErrNotSupported
|
||||
}
|
||||
fm, err := a.EventFilterManager.Install(ctx, f.Filter.MinEpoch, f.Filter.MaxEpoch, cid.Undef, f.Filter.Addresses, f.Filter.Fields, false)
|
||||
|
||||
params, err := a.parseFilter(&f.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fm, err := a.EventFilterManager.Install(ctx, params.MinHeight, params.MaxHeight, params.TipSetCid, f.Filter.Addresses, f.Filter.Fields, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+58
-58
@@ -1264,6 +1264,59 @@ func (e *EthEvent) EthGetFilterLogs(ctx context.Context, id ethtypes.EthFilterID
|
||||
return nil, xerrors.Errorf("wrong filter type")
|
||||
}
|
||||
|
||||
func parseBlockRange(heaviest abi.ChainEpoch, fromBlock, toBlock *string, maxRange abi.ChainEpoch) (minHeight abi.ChainEpoch, maxHeight abi.ChainEpoch, err error) {
|
||||
if fromBlock == nil || *fromBlock == "latest" || len(*fromBlock) == 0 {
|
||||
minHeight = heaviest
|
||||
} else if *fromBlock == "earliest" {
|
||||
minHeight = 0
|
||||
} else {
|
||||
if !strings.HasPrefix(*fromBlock, "0x") {
|
||||
return 0, 0, xerrors.Errorf("FromBlock is not a hex")
|
||||
}
|
||||
epoch, err := ethtypes.EthUint64FromHex(*fromBlock)
|
||||
if err != nil {
|
||||
return 0, 0, xerrors.Errorf("invalid epoch")
|
||||
}
|
||||
minHeight = abi.ChainEpoch(epoch)
|
||||
}
|
||||
|
||||
if toBlock == nil || *toBlock == "latest" || len(*toBlock) == 0 {
|
||||
// here latest means the latest at the time
|
||||
maxHeight = -1
|
||||
} else if *toBlock == "earliest" {
|
||||
maxHeight = 0
|
||||
} else {
|
||||
if !strings.HasPrefix(*toBlock, "0x") {
|
||||
return 0, 0, xerrors.Errorf("ToBlock is not a hex")
|
||||
}
|
||||
epoch, err := ethtypes.EthUint64FromHex(*toBlock)
|
||||
if err != nil {
|
||||
return 0, 0, 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
|
||||
if maxHeight-heaviest > maxRange {
|
||||
return 0, 0, xerrors.Errorf("invalid epoch range: to block is too far in the future (maximum: %d)", maxRange)
|
||||
}
|
||||
} else if minHeight >= 0 && maxHeight == -1 {
|
||||
// Here the client is looking for events between some time in the past and the current head
|
||||
if heaviest-minHeight > maxRange {
|
||||
return 0, 0, xerrors.Errorf("invalid epoch range: from block is too far in the past (maximum: %d)", maxRange)
|
||||
}
|
||||
} else if minHeight >= 0 && maxHeight >= 0 {
|
||||
if minHeight > maxHeight {
|
||||
return 0, 0, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", minHeight, maxHeight)
|
||||
} else if maxHeight-minHeight > maxRange {
|
||||
return 0, 0, xerrors.Errorf("invalid epoch range: range between to and from blocks is too large (maximum: %d)", maxRange)
|
||||
}
|
||||
}
|
||||
return minHeight, maxHeight, nil
|
||||
}
|
||||
|
||||
func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *ethtypes.EthFilterSpec) (*filter.EventFilter, error) {
|
||||
var (
|
||||
minHeight abi.ChainEpoch
|
||||
@@ -1280,64 +1333,11 @@ func (e *EthEvent) installEthFilterSpec(ctx context.Context, filterSpec *ethtype
|
||||
|
||||
tipsetCid = filterSpec.BlockHash.ToCid()
|
||||
} else {
|
||||
if filterSpec.FromBlock == nil || *filterSpec.FromBlock == "latest" {
|
||||
ts := e.Chain.GetHeaviestTipSet()
|
||||
minHeight = ts.Height()
|
||||
} else if *filterSpec.FromBlock == "earliest" {
|
||||
minHeight = 0
|
||||
} else if *filterSpec.FromBlock == "pending" {
|
||||
return nil, api.ErrNotSupported
|
||||
} else {
|
||||
if !strings.HasPrefix(*filterSpec.FromBlock, "0x") {
|
||||
return nil, xerrors.Errorf("FromBlock is not a hex")
|
||||
}
|
||||
epoch, err := ethtypes.EthUint64FromHex(*filterSpec.FromBlock)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("invalid epoch")
|
||||
}
|
||||
minHeight = abi.ChainEpoch(epoch)
|
||||
var err error
|
||||
minHeight, maxHeight, err = parseBlockRange(e.Chain.GetHeaviestTipSet().Height(), filterSpec.FromBlock, filterSpec.ToBlock, e.MaxFilterHeightRange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if filterSpec.ToBlock == nil || *filterSpec.ToBlock == "latest" {
|
||||
// here latest means the latest at the time
|
||||
maxHeight = -1
|
||||
} else if *filterSpec.ToBlock == "earliest" {
|
||||
maxHeight = 0
|
||||
} else if *filterSpec.ToBlock == "pending" {
|
||||
return nil, api.ErrNotSupported
|
||||
} else {
|
||||
if !strings.HasPrefix(*filterSpec.ToBlock, "0x") {
|
||||
return nil, xerrors.Errorf("ToBlock is not a hex")
|
||||
}
|
||||
epoch, err := ethtypes.EthUint64FromHex(*filterSpec.ToBlock)
|
||||
if err != nil {
|
||||
return nil, 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 nil, xerrors.Errorf("invalid epoch range: to block is too far in the future (maximum: %d)", e.MaxFilterHeightRange)
|
||||
}
|
||||
} 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 nil, xerrors.Errorf("invalid epoch range: from block is too far in the past (maximum: %d)", e.MaxFilterHeightRange)
|
||||
}
|
||||
|
||||
} else if minHeight >= 0 && maxHeight >= 0 {
|
||||
if minHeight > maxHeight {
|
||||
return nil, xerrors.Errorf("invalid epoch range: to block (%d) must be after from block (%d)", minHeight, maxHeight)
|
||||
} else if maxHeight-minHeight > e.MaxFilterHeightRange {
|
||||
return nil, xerrors.Errorf("invalid epoch range: range between to and from blocks is too large (maximum: %d)", e.MaxFilterHeightRange)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Convert all addresses to filecoin f4 addresses
|
||||
@@ -1362,7 +1362,7 @@ func keysToKeysWithCodec(keys map[string][][]byte) map[string][]types.ActorEvent
|
||||
for k, v := range keys {
|
||||
for _, vv := range v {
|
||||
keysWithCodec[k] = append(keysWithCodec[k], types.ActorEventBlock{
|
||||
Codec: uint64(multicodec.Raw),
|
||||
Codec: uint64(multicodec.Raw), // FEVM smart contract events are always encoded with the `raw` Codec.
|
||||
Value: vv,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package full
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ipfs/go-cid"
|
||||
@@ -10,12 +11,87 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
cbg "github.com/whyrusleeping/cbor-gen"
|
||||
|
||||
"github.com/filecoin-project/go-state-types/abi"
|
||||
"github.com/filecoin-project/go-state-types/big"
|
||||
|
||||
"github.com/filecoin-project/lotus/chain/types"
|
||||
"github.com/filecoin-project/lotus/chain/types/ethtypes"
|
||||
)
|
||||
|
||||
func TestParseBlockRange(t *testing.T) {
|
||||
pstring := func(s string) *string { return &s }
|
||||
|
||||
tcs := map[string]struct {
|
||||
heaviest abi.ChainEpoch
|
||||
from *string
|
||||
to *string
|
||||
maxRange abi.ChainEpoch
|
||||
minOut abi.ChainEpoch
|
||||
maxOut abi.ChainEpoch
|
||||
errStr string
|
||||
}{
|
||||
"fails when both are specified and range is greater than max allowed range": {
|
||||
heaviest: 100,
|
||||
from: pstring("0x100"),
|
||||
to: pstring("0x200"),
|
||||
maxRange: 10,
|
||||
minOut: 0,
|
||||
maxOut: 0,
|
||||
errStr: "too large",
|
||||
},
|
||||
"fails when min is specified and range is greater than max allowed range": {
|
||||
heaviest: 500,
|
||||
from: pstring("0x10"),
|
||||
to: pstring("latest"),
|
||||
maxRange: 10,
|
||||
minOut: 0,
|
||||
maxOut: 0,
|
||||
errStr: "too far in the past",
|
||||
},
|
||||
"fails when max is specified and range is greater than max allowed range": {
|
||||
heaviest: 500,
|
||||
from: pstring("earliest"),
|
||||
to: pstring("0x10000"),
|
||||
maxRange: 10,
|
||||
minOut: 0,
|
||||
maxOut: 0,
|
||||
errStr: "too large",
|
||||
},
|
||||
"works when range is valid": {
|
||||
heaviest: 500,
|
||||
from: pstring("earliest"),
|
||||
to: pstring("latest"),
|
||||
maxRange: 1000,
|
||||
minOut: 0,
|
||||
maxOut: -1,
|
||||
},
|
||||
"works when range is valid and specified": {
|
||||
heaviest: 500,
|
||||
from: pstring("0x10"),
|
||||
to: pstring("0x30"),
|
||||
maxRange: 1000,
|
||||
minOut: 16,
|
||||
maxOut: 48,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tcs {
|
||||
tc2 := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
min, max, err := parseBlockRange(tc2.heaviest, tc2.from, tc2.to, tc2.maxRange)
|
||||
require.Equal(t, tc2.minOut, min)
|
||||
require.Equal(t, tc2.maxOut, max)
|
||||
if tc2.errStr != "" {
|
||||
fmt.Println(err)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc2.errStr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEthLogFromEvent(t *testing.T) {
|
||||
// basic empty
|
||||
data, topics, ok := ethLogFromEvent(nil)
|
||||
|
||||
@@ -2,15 +2,14 @@ package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/multiformats/go-varint"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"github.com/filecoin-project/go-address"
|
||||
"github.com/filecoin-project/go-state-types/abi"
|
||||
builtintypes "github.com/filecoin-project/go-state-types/builtin"
|
||||
|
||||
"github.com/filecoin-project/lotus/chain/events"
|
||||
"github.com/filecoin-project/lotus/chain/events/filter"
|
||||
@@ -129,8 +128,9 @@ func EventFilterManager(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Loc
|
||||
fm := &filter.EventFilterManager{
|
||||
ChainStore: cs,
|
||||
EventIndex: eventIndex, // will be nil unless EnableHistoricFilterAPI is true
|
||||
// TODO:
|
||||
// We don't need this address resolution anymore once https://github.com/filecoin-project/lotus/issues/11594 lands
|
||||
AddressResolver: func(ctx context.Context, emitter abi.ActorID, ts *types.TipSet) (address.Address, bool) {
|
||||
// we only want to match using f4 addresses
|
||||
idAddr, err := address.NewIDAddress(uint64(emitter))
|
||||
if err != nil {
|
||||
return address.Undef, false
|
||||
@@ -138,18 +138,11 @@ func EventFilterManager(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Loc
|
||||
|
||||
actor, err := sm.LoadActor(ctx, idAddr, ts)
|
||||
if err != nil || actor.Address == nil {
|
||||
return address.Undef, false
|
||||
return idAddr, true
|
||||
}
|
||||
|
||||
// if robust address is not f4 then we won't match against it so bail early
|
||||
if actor.Address.Protocol() != address.Delegated {
|
||||
return address.Undef, false
|
||||
}
|
||||
// we have an f4 address, make sure it's assigned by the EAM
|
||||
// What happens when we introduce events for built-in Actor events here ?
|
||||
if namespace, _, err := varint.FromUvarint(actor.Address.Payload()); err != nil || namespace != builtintypes.EthereumAddressManagerActorID {
|
||||
return address.Undef, false
|
||||
}
|
||||
fmt.Println("")
|
||||
|
||||
return *actor.Address, true
|
||||
},
|
||||
|
||||
@@ -175,9 +168,10 @@ func ActorEventAPI(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.LockedRe
|
||||
return func(mctx helpers.MetricsCtx, r repo.LockedRepo, lc fx.Lifecycle, fm *filter.EventFilterManager, cs *store.ChainStore, sm *stmgr.StateManager, evapi EventAPI, mp *messagepool.MessagePool, stateapi full.StateAPI, chainapi full.ChainAPI) (*full.ActorEvent, error) {
|
||||
ee := &full.ActorEvent{
|
||||
MaxFilterHeightRange: abi.ChainEpoch(cfg.Events.MaxFilterHeightRange),
|
||||
Chain: cs,
|
||||
}
|
||||
|
||||
if !cfg.EnableActorEventsAPI {
|
||||
if !cfg.EnableActorEventsAPI || cfg.Events.DisableRealTimeFilterAPI {
|
||||
// all Actor events functionality is disabled
|
||||
return ee, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user