Tests for builtin actor events API

This commit is contained in:
Aarsh Shah
2024-03-22 07:00:28 +01:00
committed by Phi-rjan
parent 74a46fed3d
commit 0ca169d252
19 changed files with 519 additions and 247 deletions
+5 -5
View File
@@ -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",
+4 -3
View File
@@ -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
+57 -2
View File
@@ -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
View File
@@ -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,
})
}
+76
View File
@@ -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)
+8 -14
View File
@@ -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
}