From 69210d0917c2519dd1e9c2f5cea071b4e12ab2d2 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Thu, 10 Nov 2022 11:27:58 +0000 Subject: [PATCH 1/4] Ethereum compatible actor event API --- .circleci/config.yml | 5 + api/api_full.go | 41 ++- api/api_storage.go | 8 +- api/docgen/docgen.go | 16 +- api/eth_types.go | 184 +++++++++- api/eth_types_test.go | 214 +++++++++++ chain/events/filter/event.go | 394 ++++++++++++++++++++ chain/events/filter/event_test.go | 415 +++++++++++++++++++++ chain/events/filter/mempool.go | 141 +++++++ chain/events/filter/store.go | 93 +++++ chain/events/filter/tipset.go | 128 +++++++ itests/actor_events_test.go | 176 +++++++++ node/builder_chain.go | 6 + node/config/def.go | 21 +- node/config/types.go | 30 +- node/impl/full/eth.go | 590 +++++++++++++++++++++++++++++- node/modules/actorevent.go | 94 +++++ 17 files changed, 2533 insertions(+), 23 deletions(-) create mode 100644 chain/events/filter/event.go create mode 100644 chain/events/filter/event_test.go create mode 100644 chain/events/filter/mempool.go create mode 100644 chain/events/filter/store.go create mode 100644 chain/events/filter/tipset.go create mode 100644 itests/actor_events_test.go create mode 100644 node/modules/actorevent.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 4af2bfc12..8ca10eade 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -811,6 +811,11 @@ workflows: - gofmt - gen-check - docs-check + - test: + name: test-itest-actor_events + suite: itest-actor_events + target: "./itests/actor_events_test.go" + - test: name: test-itest-api suite: itest-api diff --git a/api/api_full.go b/api/api_full.go index d431b55ac..211cb3b59 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -26,7 +26,7 @@ import ( abinetwork "github.com/filecoin-project/go-state-types/network" apitypes "github.com/filecoin-project/lotus/api/types" - "github.com/filecoin-project/lotus/chain/actors/builtin" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/actors/builtin/power" "github.com/filecoin-project/lotus/chain/types" @@ -794,6 +794,41 @@ type FullNode interface { EthSendRawTransaction(ctx context.Context, rawTx EthBytes) (EthHash, error) //perm:read + // Returns event logs matching given filter spec. + EthGetLogs(ctx context.Context, filter *EthFilterSpec) (*EthFilterResult, error) //perm:read + + // Polling method for a filter, returns event logs which occurred since last poll. + // (requires write perm since timestamp of last filter execution will be written) + EthGetFilterChanges(ctx context.Context, id EthFilterID) (*EthFilterResult, error) //perm:write + + // Returns event logs matching filter with given id. + // (requires write perm since timestamp of last filter execution will be written) + EthGetFilterLogs(ctx context.Context, id EthFilterID) (*EthFilterResult, error) //perm:write + + // Installs a persistent filter based on given filter spec. + EthNewFilter(ctx context.Context, filter *EthFilterSpec) (EthFilterID, error) //perm:write + + // Installs a persistent filter to notify when a new block arrives. + EthNewBlockFilter(ctx context.Context) (EthFilterID, error) //perm:write + + // Installs a persistent filter to notify when new messages arrive in the message pool. + EthNewPendingTransactionFilter(ctx context.Context) (EthFilterID, error) //perm:write + + // Uninstalls a filter with given id. + EthUninstallFilter(ctx context.Context, id EthFilterID) (bool, error) //perm:write + + // Subscribe to different event types using websockets + // eventTypes is one or more of: + // - newHeads: notify when new blocks arrive. + // - pendingTransactions: notify when new messages arrive in the message pool. + // - logs: notify new event logs that match a criteria + // params contains additional parameters used with the log event type + // The client will receive a stream of EthSubscriptionResponse values until EthUnsubscribe is called. + EthSubscribe(ctx context.Context, eventTypes []string, params EthSubscriptionParams) (<-chan EthSubscriptionResponse, error) //perm:write + + // Unsubscribe from a websocket subscription + EthUnsubscribe(ctx context.Context, id EthSubscriptionID) (bool, error) //perm:write + // CreateBackup creates node backup onder the specified file name. The // method requires that the lotus daemon is running with the // LOTUS_BACKUP_BASE_PATH environment variable set to some path, and that @@ -1184,7 +1219,7 @@ type CirculatingSupply struct { type MiningBaseInfo struct { MinerPower types.BigInt NetworkPower types.BigInt - Sectors []builtin.ExtendedSectorInfo + Sectors []builtinactors.ExtendedSectorInfo WorkerKey address.Address SectorSize abi.SectorSize PrevBeaconEntry types.BeaconEntry @@ -1201,7 +1236,7 @@ type BlockTemplate struct { Messages []*types.SignedMessage Epoch abi.ChainEpoch Timestamp uint64 - WinningPoStProof []builtin.PoStProof + WinningPoStProof []builtinactors.PoStProof } type DataSize struct { diff --git a/api/api_storage.go b/api/api_storage.go index 100be5cca..b4a0cc5f7 100644 --- a/api/api_storage.go +++ b/api/api_storage.go @@ -22,7 +22,7 @@ import ( "github.com/filecoin-project/go-state-types/builtin/v9/miner" abinetwork "github.com/filecoin-project/go-state-types/network" - "github.com/filecoin-project/lotus/chain/actors/builtin" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/storage/pipeline/sealiface" "github.com/filecoin-project/lotus/storage/sealer/fsutil" @@ -152,7 +152,7 @@ type StorageMiner interface { WorkerStats(context.Context) (map[uuid.UUID]storiface.WorkerStats, error) //perm:admin WorkerJobs(context.Context) (map[uuid.UUID][]storiface.WorkerJob, error) //perm:admin - //storiface.WorkerReturn + // storiface.WorkerReturn ReturnDataCid(ctx context.Context, callID storiface.CallID, pi abi.PieceInfo, err *storiface.CallError) error //perm:admin retry:true ReturnAddPiece(ctx context.Context, callID storiface.CallID, pi abi.PieceInfo, err *storiface.CallError) error //perm:admin retry:true ReturnSealPreCommit1(ctx context.Context, callID storiface.CallID, p1o storiface.PreCommit1Out, err *storiface.CallError) error //perm:admin retry:true @@ -175,7 +175,7 @@ type StorageMiner interface { // SealingSchedDiag dumps internal sealing scheduler state SealingSchedDiag(ctx context.Context, doSched bool) (interface{}, error) //perm:admin SealingAbort(ctx context.Context, call storiface.CallID) error //perm:admin - //SealingSchedRemove removes a request from sealing pipeline + // SealingSchedRemove removes a request from sealing pipeline SealingRemoveRequest(ctx context.Context, schedId uuid.UUID) error //perm:admin // paths.SectorIndex @@ -322,7 +322,7 @@ type StorageMiner interface { CheckProvable(ctx context.Context, pp abi.RegisteredPoStProof, sectors []storiface.SectorRef, expensive bool) (map[abi.SectorNumber]string, error) //perm:admin - ComputeProof(ctx context.Context, ssi []builtin.ExtendedSectorInfo, rand abi.PoStRandomness, poStEpoch abi.ChainEpoch, nv abinetwork.Version) ([]builtin.PoStProof, error) //perm:read + ComputeProof(ctx context.Context, ssi []builtinactors.ExtendedSectorInfo, rand abi.PoStRandomness, poStEpoch abi.ChainEpoch, nv abinetwork.Version) ([]builtinactors.PoStProof, error) //perm:read // RecoverFault can be used to declare recoveries manually. It sends messages // to the miner actor with details of recovered sectors and returns the CID of messages. It honors the diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index fc6c82157..b27df57dd 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -298,7 +298,8 @@ func init() { "title": "Lotus RPC API", "version": "1.2.1/generated=2020-11-22T08:22:42-06:00", }, - "methods": []interface{}{}}, + "methods": []interface{}{}, + }, ) addExample(api.CheckStatusCode(0)) @@ -335,7 +336,8 @@ func init() { NumConnsInbound: 3, NumConnsOutbound: 4, NumFD: 5, - }}) + }, + }) addExample(api.NetLimit{ Memory: 123, StreamsInbound: 1, @@ -374,10 +376,18 @@ func init() { ethFeeHistoryReward := [][]api.EthBigInt{} addExample(ðFeeHistoryReward) + + addExample(api.EthFilterID("c5564560217c43e4bc0484df655e9019")) + addExample(api.EthSubscriptionID("b62df77831484129adf6682332ad0725")) + + pstring := func(s string) *string { return &s } + addExample(&api.EthFilterSpec{ + FromBlock: pstring("2301220"), + Address: []api.EthAddress{ethaddr}, + }) } func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { - switch pkg { case "api": // latest switch name { diff --git a/api/eth_types.go b/api/eth_types.go index 03bf85e05..d8dd6e64d 100644 --- a/api/eth_types.go +++ b/api/eth_types.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/ipfs/go-cid" + "github.com/minio/blake2b-simd" "github.com/multiformats/go-multihash" "github.com/multiformats/go-varint" "golang.org/x/xerrors" @@ -48,9 +49,7 @@ func (e *EthUint64) UnmarshalJSON(b []byte) error { type EthBigInt big.Int -var ( - EthBigIntZero = EthBigInt{Int: big.Zero().Int} -) +var EthBigIntZero = EthBigInt{Int: big.Zero().Int} func (e EthBigInt) MarshalJSON() ([]byte, error) { if e.Int == nil { @@ -396,7 +395,6 @@ func handlePrefix(s *string) { func decodeHexString(s string, length int) ([]byte, error) { b, err := hex.DecodeString(s) - if err != nil { return []byte{}, xerrors.Errorf("cannot parse hash: %w", err) } @@ -423,6 +421,10 @@ func EthHashFromHex(s string) (EthHash, error) { return h, nil } +func EthHashData(b []byte) EthHash { + return EthHash(blake2b.Sum256(b)) +} + func (h EthHash) String() string { return "0x" + hex.EncodeToString(h[:]) } @@ -440,3 +442,177 @@ type EthFeeHistory struct { GasUsedRatio []float64 `json:"gasUsedRatio"` Reward *[][]EthBigInt `json:"reward,omitempty"` } + +// An opaque identifier generated by the Lotus node to refer to an installed filter. +type EthFilterID string + +// An opaque identifier generated by the Lotus node to refer to an active subscription. +type EthSubscriptionID string + +type EthFilterSpec struct { + // Interpreted as an epoch or one of "latest" for last mined block, "earliest" for first, + // "pending" for not yet committed messages. + // Optional, default: "latest". + FromBlock *string `json:"fromBlock,omitempty"` + + // Interpreted as an epoch or one of "latest" for last mined block, "earliest" for first, + // "pending" for not yet committed messages. + // Optional, default: "latest". + ToBlock *string `json:"toBlock,omitempty"` + + // Actor address or a list of addresses from which event logs should originate. + // Optional, default nil. + // The JSON decoding must treat a string as equivalent to an array with one value, for example + // "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] + Address EthAddressList `json:"address"` + + // List of topics to be matched. + // Optional, default: empty list + Topics EthTopicSpec `json:"topics"` + + // Restricts event logs returned to those in receipts contained in the tipset this block is part of. + // If BlockHash is present in in the filter criteria, then neither FromBlock nor ToBlock are allowed. + // Added in EIP-234 + BlockHash *EthHash `json:"blockHash,omitempty"` +} + +// EthAddressSpec represents a list of addresses. +// The JSON decoding must treat a string as equivalent to an array with one value, for example +// "0x8888f1f195afa192cfee86069858" must be decoded as [ "0x8888f1f195afa192cfee86069858" ] +type EthAddressList []EthAddress + +func (e *EthAddressList) UnmarshalJSON(b []byte) error { + if len(b) > 0 && b[0] == '[' { + var addrs []EthAddress + err := json.Unmarshal(b, &addrs) + if err != nil { + return err + } + *e = addrs + return nil + } + var addr EthAddress + err := json.Unmarshal(b, &addr) + if err != nil { + return err + } + *e = []EthAddress{addr} + return nil +} + +// TopicSpec represents a specification for matching by topic. An empty spec means all topics +// will be matched. Otherwise topics are matched conjunctively in the first dimension of the +// slice and disjunctively in the second dimension. Topics are matched in order. +// An event log with topics [A, B] will be matched by the following topic specs: +// [] "all" +// [[A]] "A in first position (and anything after)" +// [[A]] "A in first position (and anything after)" +// [nil, [B] ] "anything in first position AND B in second position (and anything after)" +// [[A], [B]] "A in first position AND B in second position (and anything after)" +// [[A, B], [A, B]] "(A OR B) in first position AND (A OR B) in second position (and anything after)" +// +// The JSON decoding must treat string values as equivalent to arrays with one value, for example +// { "A", [ "B", "C" ] } must be decoded as [ [ A ], [ B, C ] ] +type EthTopicSpec []EthHashList + +type EthHashList []EthHash + +func (e *EthHashList) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, []byte{'n', 'u', 'l', 'l'}) { + return nil + } + if len(b) > 0 && b[0] == '[' { + var hashes []EthHash + err := json.Unmarshal(b, &hashes) + if err != nil { + return err + } + *e = hashes + return nil + } + var hash EthHash + err := json.Unmarshal(b, &hash) + if err != nil { + return err + } + *e = []EthHash{hash} + return nil +} + +// FilterResult represents the response from executing a filter: a list of bloack hashes, a list of transaction hashes +// or a list of logs +// This is a union type. Only one field will be populated. +// The JSON encoding must produce an array of the populated field. +type EthFilterResult struct { + // List of block hashes. Only populated when the filter has been installed via EthNewBlockFilter + NewBlockHashes []EthHash + + // List of transaction hashes. Only populated when the filter has been installed via EthNewPendingTransactionFilter + NewTransactionHashes []EthHash + + // List of event logs. Only populated when the filter has been installed via EthNewFilter + NewLogs []EthLog +} + +func (h EthFilterResult) MarshalJSON() ([]byte, error) { + if h.NewBlockHashes != nil { + return json.Marshal(h.NewBlockHashes) + } + if h.NewTransactionHashes != nil { + return json.Marshal(h.NewTransactionHashes) + } + if h.NewLogs != nil { + return json.Marshal(h.NewLogs) + } + return []byte{'[', ']'}, nil +} + +type EthLog struct { + // Address is the address of the actor that produced the event log. + Address EthAddress `json:"address"` + + // Data is the values of the event log, excluding topics + Data []EthHash `json:"data"` + + // List of topics associated with the event log. + Topics []EthHash `json:"topics"` + + // Following fields are derived from the transaction containing the log + + // Indicates whether the log was removed due to a chain reorganization. + Removed bool `json:"removed"` + + // LogIndex is the index of the event log in the sequence of events produced by the message execution. + // (this is the index in the events AMT on the message receipt) + LogIndex EthUint64 `json:"logIndex"` + + // TransactionIndex is the index in the tipset of the transaction that produced the event log. + // The index corresponds to the sequence of messages produced by ChainGetParentMessages + TransactionIndex EthUint64 `json:"transactionIndex"` + + // TransactionHash is the cid of the transaction that produced the event log. + TransactionHash EthHash `json:"transactionHash"` + + // BlockHash is the hash of a block in the tipset containing the message receipt of the message execution. + // This may be passed to ChainGetParentReceipts to obtain a list of receipts. The receipt + // containing the events will be at TransactionIndex in the receipt list. + BlockHash EthHash `json:"blockHash"` + + // BlockNumber is the epoch at which the message was executed. This is the epoch containing + // the message receipt. + BlockNumber EthUint64 `json:"blockNumber"` +} + +type EthSubscriptionParams struct { + // List of topics to be matched. + // Optional, default: empty list + Topics EthTopicSpec `json:"topics,omitempty"` +} + +type EthSubscriptionResponse struct { + // The persistent identifier for the subscription which can be used to unsubscribe. + SubscriptionID EthSubscriptionID `json:"subscription"` + + // The object matching the subscription. This may be a Block (tipset), a Transaction (message) or an EthLog + Result interface{} `json:"result"` +} diff --git a/api/eth_types_test.go b/api/eth_types_test.go index 46ce4f49a..cea465965 100644 --- a/api/eth_types_test.go +++ b/api/eth_types_test.go @@ -2,6 +2,7 @@ package api import ( + "encoding/json" "strings" "testing" @@ -30,6 +31,7 @@ func TestEthIntMarshalJSON(t *testing.T) { require.Equal(t, j, tc.Output) } } + func TestEthIntUnmarshalJSON(t *testing.T) { testcases := []TestCase{ {[]byte("\"0x0\""), EthUint64(0)}, @@ -155,3 +157,215 @@ func TestUnmarshalEthBytes(t *testing.T) { require.Equal(t, string(data), tc) } } + +func TestEthFilterResultMarshalJSON(t *testing.T) { + hash1, err := EthHashFromHex("013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184") + require.NoError(t, err, "eth hash") + + hash2, err := EthHashFromHex("ab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738") + require.NoError(t, err, "eth hash") + + addr, err := EthAddressFromHex("d4c5fb16488Aa48081296299d54b0c648C9333dA") + require.NoError(t, err, "eth address") + + log := EthLog{ + Removed: true, + LogIndex: 5, + TransactionIndex: 45, + TransactionHash: hash1, + BlockHash: hash2, + BlockNumber: 53, + Topics: []EthHash{hash1}, + Data: []EthHash{hash1}, + Address: addr, + } + logjson, err := json.Marshal(log) + require.NoError(t, err, "log json") + + testcases := []struct { + res EthFilterResult + want string + }{ + { + res: EthFilterResult{}, + want: "[]", + }, + + { + res: EthFilterResult{ + NewBlockHashes: []EthHash{hash1, hash2}, + }, + want: `["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184","0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738"]`, + }, + + { + res: EthFilterResult{ + NewTransactionHashes: []EthHash{hash1, hash2}, + }, + want: `["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184","0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738"]`, + }, + + { + res: EthFilterResult{ + NewLogs: []EthLog{log}, + }, + want: `[` + string(logjson) + `]`, + }, + } + + for _, tc := range testcases { + data, err := json.Marshal(tc.res) + require.NoError(t, err) + require.Equal(t, tc.want, string(data)) + } +} + +func TestEthFilterSpecUnmarshalJSON(t *testing.T) { + hash1, err := EthHashFromHex("013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184") + require.NoError(t, err, "eth hash") + + hash2, err := EthHashFromHex("ab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738") + require.NoError(t, err, "eth hash") + + addr, err := EthAddressFromHex("d4c5fb16488Aa48081296299d54b0c648C9333dA") + require.NoError(t, err, "eth address") + + pstring := func(s string) *string { return &s } + phash := func(h EthHash) *EthHash { return &h } + + testcases := []struct { + input string + want EthFilterSpec + }{ + { + input: `{"fromBlock":"latest"}`, + want: EthFilterSpec{FromBlock: pstring("latest")}, + }, + { + input: `{"toBlock":"pending"}`, + want: EthFilterSpec{ToBlock: pstring("pending")}, + }, + { + input: `{"address":["0xd4c5fb16488Aa48081296299d54b0c648C9333dA"]}`, + want: EthFilterSpec{Address: EthAddressList{addr}}, + }, + { + input: `{"address":"0xd4c5fb16488Aa48081296299d54b0c648C9333dA"}`, + want: EthFilterSpec{Address: EthAddressList{addr}}, + }, + { + input: `{"blockHash":"0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184"}`, + want: EthFilterSpec{BlockHash: phash(hash1)}, + }, + { + input: `{"topics":["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184"]}`, + want: EthFilterSpec{ + Topics: EthTopicSpec{ + {hash1}, + }, + }, + }, + { + input: `{"topics":["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184","0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738"]}`, + want: EthFilterSpec{ + Topics: EthTopicSpec{ + {hash1}, + {hash2}, + }, + }, + }, + { + input: `{"topics":[null, ["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184","0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738"]]}`, + want: EthFilterSpec{ + Topics: EthTopicSpec{ + nil, + {hash1, hash2}, + }, + }, + }, + { + input: `{"topics":[null, "0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184"]}`, + want: EthFilterSpec{ + Topics: EthTopicSpec{ + nil, + {hash1}, + }, + }, + }, + } + + for _, tc := range testcases { + var got EthFilterSpec + err := json.Unmarshal([]byte(tc.input), &got) + require.NoError(t, err) + require.Equal(t, tc.want, got) + } +} + +func TestEthAddressListUnmarshalJSON(t *testing.T) { + addr1, err := EthAddressFromHex("d4c5fb16488Aa48081296299d54b0c648C9333dA") + require.NoError(t, err, "eth address") + + addr2, err := EthAddressFromHex("abbbfb16488Aa48081296299d54b0c648C9333dA") + require.NoError(t, err, "eth address") + + testcases := []struct { + input string + want EthAddressList + }{ + { + input: `["0xd4c5fb16488Aa48081296299d54b0c648C9333dA"]`, + want: EthAddressList{addr1}, + }, + { + input: `["0xd4c5fb16488Aa48081296299d54b0c648C9333dA","abbbfb16488Aa48081296299d54b0c648C9333dA"]`, + want: EthAddressList{addr1, addr2}, + }, + { + input: `"0xd4c5fb16488Aa48081296299d54b0c648C9333dA"`, + want: EthAddressList{addr1}, + }, + } + for _, tc := range testcases { + var got EthAddressList + err := json.Unmarshal([]byte(tc.input), &got) + require.NoError(t, err) + require.Equal(t, tc.want, got) + } +} + +func TestEthHashListUnmarshalJSON(t *testing.T) { + hash1, err := EthHashFromHex("013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184") + require.NoError(t, err, "eth hash") + + hash2, err := EthHashFromHex("ab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738") + require.NoError(t, err, "eth hash") + + testcases := []struct { + input string + want *EthHashList + }{ + { + input: `["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184"]`, + want: &EthHashList{hash1}, + }, + { + input: `["0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184","0xab8653edf9f51785664a643b47605a7ba3d917b5339a0724e7642c114d0e4738"]`, + want: &EthHashList{hash1, hash2}, + }, + { + input: `"0x013dbb9442ca9667baccc6230fcd5c1c4b2d4d2870f4bd20681d4d47cfd15184"`, + want: &EthHashList{hash1}, + }, + { + input: `null`, + want: nil, + }, + } + for _, tc := range testcases { + var got *EthHashList + err := json.Unmarshal([]byte(tc.input), &got) + require.NoError(t, err) + require.Equal(t, tc.want, got) + } +} diff --git a/chain/events/filter/event.go b/chain/events/filter/event.go new file mode 100644 index 000000000..c81aed7fe --- /dev/null +++ b/chain/events/filter/event.go @@ -0,0 +1,394 @@ +package filter + +import ( + "bytes" + "context" + "sync" + "time" + + "github.com/google/uuid" + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + blockadt "github.com/filecoin-project/specs-actors/actors/util/adt" + + cstore "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" +) + +const indexed uint8 = 0x01 + +type EventFilter struct { + id string + minHeight abi.ChainEpoch // minimum epoch to apply filter or -1 if no minimum + maxHeight abi.ChainEpoch // maximum epoch to apply filter or -1 if no maximum + tipsetCid cid.Cid + addresses []address.Address // list of actor ids that originated the event + keys map[string][][]byte // map of key names to a list of alternate values that may match + maxResults int // maximum number of results to collect, 0 is unlimited + + mu sync.Mutex + collected []*CollectedEvent + lastTaken time.Time + ch chan<- interface{} +} + +var _ Filter = (*EventFilter)(nil) + +type CollectedEvent struct { + Event *types.Event + EventIdx int // index of the event within the list of emitted events + Reverted bool + Height abi.ChainEpoch + TipSetKey types.TipSetKey // tipset that contained the message + MsgIdx int // index of the message in the tipset + MsgCid cid.Cid // cid of message that produced event +} + +func (f *EventFilter) ID() string { + return f.id +} + +func (f *EventFilter) SetSubChannel(ch chan<- interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = ch + f.collected = nil +} + +func (f *EventFilter) ClearSubChannel() { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = nil +} + +func (f *EventFilter) CollectEvents(ctx context.Context, te *TipSetEvents, revert bool) error { + if !f.matchTipset(te) { + return nil + } + + ems, err := te.messages(ctx) + if err != nil { + return xerrors.Errorf("load executed messages: %w", err) + } + for msgIdx, em := range ems { + for evIdx, ev := range em.Events() { + if !f.matchAddress(ev.Emitter) { + continue + } + if !f.matchKeys(ev.Entries) { + continue + } + + // event matches filter, so record it + cev := &CollectedEvent{ + Event: ev, + EventIdx: evIdx, + Reverted: revert, + Height: te.msgTs.Height(), + TipSetKey: te.msgTs.Key(), + MsgCid: em.Message().Cid(), + MsgIdx: msgIdx, + } + + f.mu.Lock() + // if we have a subscription channel then push event to it + if f.ch != nil { + f.ch <- cev + f.mu.Unlock() + continue + } + + if f.maxResults > 0 && len(f.collected) == f.maxResults { + copy(f.collected, f.collected[1:]) + f.collected = f.collected[:len(f.collected)-1] + } + f.collected = append(f.collected, cev) + f.mu.Unlock() + } + } + + return nil +} + +func (f *EventFilter) TakeCollectedEvents(ctx context.Context) []*CollectedEvent { + f.mu.Lock() + collected := f.collected + f.collected = nil + f.lastTaken = time.Now().UTC() + f.mu.Unlock() + + return collected +} + +func (f *EventFilter) LastTaken() time.Time { + f.mu.Lock() + defer f.mu.Unlock() + return f.lastTaken +} + +// matchTipset reports whether this filter matches the given tipset +func (f *EventFilter) matchTipset(te *TipSetEvents) bool { + if f.tipsetCid != cid.Undef { + tsCid, err := te.Cid() + if err != nil { + return false + } + return f.tipsetCid.Equals(tsCid) + } + + if f.minHeight >= 0 && f.minHeight > te.Height() { + return false + } + if f.maxHeight >= 0 && f.maxHeight < te.Height() { + return false + } + return true +} + +func (f *EventFilter) matchAddress(o address.Address) bool { + if len(f.addresses) == 0 { + return true + } + // Assume short lists of addresses + // TODO: binary search for longer lists + for _, a := range f.addresses { + if a == o { + return true + } + } + return false +} + +func (f *EventFilter) matchKeys(ees []types.EventEntry) bool { + if len(f.keys) == 0 { + return true + } + // TODO: optimize this naive algorithm + // Note keys names may be repeated so we may have multiple opportunities to match + + matched := map[string]bool{} + for _, ee := range ees { + // Skip an entry that is not indexable + if ee.Flags&indexed != indexed { + continue + } + + keyname := string(ee.Key) + + // skip if we have already matched this key + if matched[keyname] { + continue + } + + wantlist, ok := f.keys[keyname] + if !ok { + continue + } + + for _, w := range wantlist { + if bytes.Equal(w, ee.Value) { + matched[keyname] = true + break + } + } + + if len(matched) == len(f.keys) { + // all keys have been matched + return true + } + + } + + return false +} + +type TipSetEvents struct { + rctTs *types.TipSet // rctTs is the tipset containing the receipts of executed messages + msgTs *types.TipSet // msgTs is the tipset containing the messages that have been executed + + load func(ctx context.Context, msgTs, rctTs *types.TipSet) ([]executedMessage, error) + + once sync.Once // for lazy population of ems + ems []executedMessage + err error +} + +func (te *TipSetEvents) Height() abi.ChainEpoch { + return te.msgTs.Height() +} + +func (te *TipSetEvents) Cid() (cid.Cid, error) { + return te.msgTs.Key().Cid() +} + +func (te *TipSetEvents) messages(ctx context.Context) ([]executedMessage, error) { + te.once.Do(func() { + // populate executed message list + ems, err := te.load(ctx, te.msgTs, te.rctTs) + if err != nil { + te.err = err + return + } + te.ems = ems + }) + return te.ems, te.err +} + +type executedMessage struct { + msg *types.Message + rct *types.MessageReceipt + // events extracted from receipt + evs []*types.Event +} + +func (e *executedMessage) Message() *types.Message { + return e.msg +} + +func (e *executedMessage) Receipt() *types.MessageReceipt { + return e.rct +} + +func (e *executedMessage) Events() []*types.Event { + return e.evs +} + +type EventFilterManager struct { + ChainStore *cstore.ChainStore + MaxFilterResults int + + mu sync.Mutex // guards mutations to filters + filters map[string]*EventFilter +} + +func (m *EventFilterManager) Apply(ctx context.Context, from, to *types.TipSet) error { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.filters) == 0 { + return nil + } + + tse := &TipSetEvents{ + msgTs: from, + rctTs: to, + load: m.loadExecutedMessages, + } + + // TODO: could run this loop in parallel with errgroup if there are many filters + for _, f := range m.filters { + if err := f.CollectEvents(ctx, tse, false); err != nil { + return err + } + } + + return nil +} + +func (m *EventFilterManager) Revert(ctx context.Context, from, to *types.TipSet) error { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.filters) == 0 { + return nil + } + + tse := &TipSetEvents{ + msgTs: to, + rctTs: from, + load: m.loadExecutedMessages, + } + + // TODO: could run this loop in parallel with errgroup if there are many filters + for _, f := range m.filters { + if err := f.CollectEvents(ctx, tse, true); err != nil { + return err + } + } + + return nil +} + +func (m *EventFilterManager) Install(ctx context.Context, minHeight, maxHeight abi.ChainEpoch, tipsetCid cid.Cid, addresses []address.Address, keys map[string][][]byte) (*EventFilter, error) { + id, err := uuid.NewRandom() + if err != nil { + return nil, xerrors.Errorf("new uuid: %w", err) + } + + f := &EventFilter{ + id: id.String(), + minHeight: minHeight, + maxHeight: maxHeight, + tipsetCid: tipsetCid, + addresses: addresses, + keys: keys, + maxResults: m.MaxFilterResults, + } + + m.mu.Lock() + m.filters[id.String()] = f + m.mu.Unlock() + + return f, nil +} + +func (m *EventFilterManager) Remove(ctx context.Context, id string) error { + m.mu.Lock() + defer m.mu.Unlock() + if _, found := m.filters[id]; !found { + return ErrFilterNotFound + } + delete(m.filters, id) + return nil +} + +func (m *EventFilterManager) loadExecutedMessages(ctx context.Context, msgTs, rctTs *types.TipSet) ([]executedMessage, error) { + msgs, err := m.ChainStore.MessagesForTipset(ctx, msgTs) + if err != nil { + return nil, xerrors.Errorf("read messages: %w", err) + } + + st := m.ChainStore.ActorStore(ctx) + + arr, err := blockadt.AsArray(st, rctTs.Blocks()[0].ParentMessageReceipts) + if err != nil { + return nil, xerrors.Errorf("load receipts amt: %w", err) + } + + if uint64(len(msgs)) != arr.Length() { + return nil, xerrors.Errorf("mismatching message and receipt counts (%d msgs, %d rcts)", len(msgs), arr.Length()) + } + + ems := make([]executedMessage, len(msgs)) + + for i := 0; i < len(msgs); i++ { + ems[i].msg = msgs[i].VMMessage() + + var rct types.MessageReceipt + found, err := arr.Get(uint64(i), &rct) + if err != nil { + return nil, xerrors.Errorf("load receipt: %w", err) + } + if !found { + return nil, xerrors.Errorf("receipt %d not found", i) + } + ems[i].rct = &rct + + evtArr, err := blockadt.AsArray(st, rct.EventsRoot) + if err != nil { + return nil, xerrors.Errorf("load events amt: %w", err) + } + + ems[i].evs = make([]*types.Event, evtArr.Length()) + var evt types.Event + _ = arr.ForEach(&evt, func(i int64) error { + cpy := evt + ems[i].evs[int(i)] = &cpy + return nil + }) + + } + + return ems, nil +} diff --git a/chain/events/filter/event_test.go b/chain/events/filter/event_test.go new file mode 100644 index 000000000..76cad096e --- /dev/null +++ b/chain/events/filter/event_test.go @@ -0,0 +1,415 @@ +package filter + +import ( + "context" + pseudo "math/rand" + "testing" + + "github.com/ipfs/go-cid" + cbor "github.com/ipfs/go-ipld-cbor" + mh "github.com/multiformats/go-multihash" + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/crypto" + "github.com/filecoin-project/go-state-types/exitcode" + blockadt "github.com/filecoin-project/specs-actors/actors/util/adt" + + "github.com/filecoin-project/lotus/blockstore" + "github.com/filecoin-project/lotus/chain/actors/adt" + "github.com/filecoin-project/lotus/chain/types" +) + +func TestEventFilterCollectEvents(t *testing.T) { + rng := pseudo.New(pseudo.NewSource(299792458)) + a1 := randomActorAddr(t, rng) + a2 := randomActorAddr(t, rng) + + ev1 := fakeEvent( + a1, + []kv{ + {k: "type", v: []byte("approval")}, + {k: "signer", v: []byte("addr1")}, + }, + []kv{ + {k: "amount", v: []byte("2988181")}, + }, + ) + + st := newStore() + events := []*types.Event{ev1} + em := executedMessage{ + msg: fakeMessage(randomActorAddr(t, rng), randomActorAddr(t, rng)), + rct: fakeReceipt(t, rng, st, events), + evs: events, + } + + events14000 := buildTipSetEvents(t, rng, 14000, em) + cid14000, err := events14000.msgTs.Key().Cid() + require.NoError(t, err, "tipset cid") + + noCollectedEvents := []*CollectedEvent{} + oneCollectedEvent := []*CollectedEvent{ + { + Event: ev1, + EventIdx: 0, + Reverted: false, + Height: 14000, + TipSetKey: events14000.msgTs.Key(), + MsgIdx: 0, + MsgCid: em.msg.Cid(), + }, + } + + testCases := []struct { + name string + filter *EventFilter + te *TipSetEvents + want []*CollectedEvent + }{ + { + name: "nomatch tipset min height", + filter: &EventFilter{ + minHeight: 14001, + maxHeight: -1, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "nomatch tipset max height", + filter: &EventFilter{ + minHeight: -1, + maxHeight: 13999, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "match tipset min height", + filter: &EventFilter{ + minHeight: 14000, + maxHeight: -1, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "match tipset cid", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + tipsetCid: cid14000, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "nomatch address", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + addresses: []address.Address{a2}, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "match address", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + addresses: []address.Address{a1}, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "match one entry", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("approval"), + }, + }, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "match one entry with alternate values", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("cancel"), + []byte("propose"), + []byte("approval"), + }, + }, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "nomatch one entry by missing value", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("cancel"), + []byte("propose"), + }, + }, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "nomatch one entry by missing key", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "method": { + []byte("approval"), + }, + }, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "match one entry with multiple keys", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("approval"), + }, + "signer": { + []byte("addr1"), + }, + }, + }, + te: events14000, + want: oneCollectedEvent, + }, + { + name: "nomatch one entry with one mismatching key", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("approval"), + }, + "approver": { + []byte("addr1"), + }, + }, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "nomatch one entry with one mismatching value", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "type": { + []byte("approval"), + }, + "signer": { + []byte("addr2"), + }, + }, + }, + te: events14000, + want: noCollectedEvents, + }, + { + name: "nomatch one entry with one unindexed key", + filter: &EventFilter{ + minHeight: -1, + maxHeight: -1, + keys: map[string][][]byte{ + "amount": { + []byte("2988181"), + }, + }, + }, + te: events14000, + want: noCollectedEvents, + }, + } + + for _, tc := range testCases { + tc := tc // appease lint + t.Run(tc.name, func(t *testing.T) { + if err := tc.filter.CollectEvents(context.Background(), tc.te, false); err != nil { + require.NoError(t, err, "collect events") + } + + coll := tc.filter.TakeCollectedEvents(context.Background()) + require.ElementsMatch(t, coll, tc.want) + }) + } +} + +type kv struct { + k string + v []byte +} + +func fakeEvent(emitter address.Address, indexed []kv, unindexed []kv) *types.Event { + ev := &types.Event{ + Emitter: emitter, + } + + for _, in := range indexed { + ev.Entries = append(ev.Entries, types.EventEntry{ + Flags: 0x01, + Key: []byte(in.k), + Value: in.v, + }) + } + + for _, in := range unindexed { + ev.Entries = append(ev.Entries, types.EventEntry{ + Flags: 0x00, + Key: []byte(in.k), + Value: in.v, + }) + } + + return ev +} + +func randomActorAddr(tb testing.TB, rng *pseudo.Rand) address.Address { + tb.Helper() + addr, err := address.NewActorAddress(randomBytes(32, rng)) + require.NoError(tb, err) + + return addr +} + +func randomIDAddr(tb testing.TB, rng *pseudo.Rand) address.Address { + tb.Helper() + addr, err := address.NewIDAddress(uint64(rng.Int63())) + require.NoError(tb, err) + return addr +} + +func randomCid(tb testing.TB, rng *pseudo.Rand) cid.Cid { + tb.Helper() + cb := cid.V1Builder{Codec: cid.Raw, MhType: mh.IDENTITY} + c, err := cb.Sum(randomBytes(10, rng)) + require.NoError(tb, err) + return c +} + +func randomBytes(n int, rng *pseudo.Rand) []byte { + buf := make([]byte, n) + rng.Read(buf) + return buf +} + +func fakeMessage(to, from address.Address) *types.Message { + return &types.Message{ + To: to, + From: from, + Nonce: 197, + Method: 1, + Params: []byte("some random bytes"), + GasLimit: 126723, + GasPremium: types.NewInt(4), + GasFeeCap: types.NewInt(120), + } +} + +func fakeReceipt(tb testing.TB, rng *pseudo.Rand, st adt.Store, events []*types.Event) *types.MessageReceipt { + arr := blockadt.MakeEmptyArray(st) + for _, ev := range events { + err := arr.AppendContinuous(ev) + require.NoError(tb, err, "append event") + } + eventsRoot, err := arr.Root() + require.NoError(tb, err, "flush events amt") + + return &types.MessageReceipt{ + ExitCode: exitcode.Ok, + Return: randomBytes(32, rng), + GasUsed: rng.Int63(), + Events: eventsRoot, + } +} + +func fakeTipSet(tb testing.TB, rng *pseudo.Rand, h abi.ChainEpoch, parents []cid.Cid) *types.TipSet { + tb.Helper() + ts, err := types.NewTipSet([]*types.BlockHeader{ + { + Height: h, + Miner: randomIDAddr(tb, rng), + + Parents: parents, + + Ticket: &types.Ticket{VRFProof: []byte{byte(h % 2)}}, + + ParentStateRoot: randomCid(tb, rng), + Messages: randomCid(tb, rng), + ParentMessageReceipts: randomCid(tb, rng), + + BlockSig: &crypto.Signature{Type: crypto.SigTypeBLS}, + BLSAggregate: &crypto.Signature{Type: crypto.SigTypeBLS}, + }, + { + Height: h, + Miner: randomIDAddr(tb, rng), + + Parents: parents, + + Ticket: &types.Ticket{VRFProof: []byte{byte((h + 1) % 2)}}, + + ParentStateRoot: randomCid(tb, rng), + Messages: randomCid(tb, rng), + ParentMessageReceipts: randomCid(tb, rng), + + BlockSig: &crypto.Signature{Type: crypto.SigTypeBLS}, + BLSAggregate: &crypto.Signature{Type: crypto.SigTypeBLS}, + }, + }) + + require.NoError(tb, err) + + return ts +} + +func newStore() adt.Store { + ctx := context.Background() + bs := blockstore.NewMemorySync() + store := cbor.NewCborStore(bs) + return adt.WrapStore(ctx, store) +} + +func buildTipSetEvents(tb testing.TB, rng *pseudo.Rand, h abi.ChainEpoch, em executedMessage) *TipSetEvents { + tb.Helper() + + msgTs := fakeTipSet(tb, rng, h, []cid.Cid{}) + rctTs := fakeTipSet(tb, rng, h+1, msgTs.Cids()) + + return &TipSetEvents{ + msgTs: msgTs, + rctTs: rctTs, + load: func(ctx context.Context, msgTs, rctTs *types.TipSet) ([]executedMessage, error) { + return []executedMessage{em}, nil + }, + } +} diff --git a/chain/events/filter/mempool.go b/chain/events/filter/mempool.go new file mode 100644 index 000000000..dcea0f54c --- /dev/null +++ b/chain/events/filter/mempool.go @@ -0,0 +1,141 @@ +package filter + +import ( + "context" + "sync" + "time" + + "github.com/google/uuid" + "github.com/ipfs/go-cid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/types" +) + +type MemPoolFilter struct { + id string + maxResults int // maximum number of results to collect, 0 is unlimited + ch chan<- interface{} + + mu sync.Mutex + collected []cid.Cid + lastTaken time.Time +} + +var _ Filter = (*MemPoolFilter)(nil) + +func (f *MemPoolFilter) ID() string { + return f.id +} + +func (f *MemPoolFilter) SetSubChannel(ch chan<- interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = ch + f.collected = nil +} + +func (f *MemPoolFilter) ClearSubChannel() { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = nil +} + +func (f *MemPoolFilter) CollectMessage(ctx context.Context, msg *types.SignedMessage) { + f.mu.Lock() + defer f.mu.Unlock() + + // if we have a subscription channel then push message to it + if f.ch != nil { + f.ch <- msg + return + } + + if f.maxResults > 0 && len(f.collected) == f.maxResults { + copy(f.collected, f.collected[1:]) + f.collected = f.collected[:len(f.collected)-1] + } + f.collected = append(f.collected, msg.Cid()) +} + +func (f *MemPoolFilter) TakeCollectedMessages(context.Context) []cid.Cid { + f.mu.Lock() + collected := f.collected + f.collected = nil + f.lastTaken = time.Now().UTC() + f.mu.Unlock() + + return collected +} + +func (f *MemPoolFilter) LastTaken() time.Time { + f.mu.Lock() + defer f.mu.Unlock() + return f.lastTaken +} + +type MemPoolFilterManager struct { + MaxFilterResults int + + mu sync.Mutex // guards mutations to filters + filters map[string]*MemPoolFilter +} + +func (m *MemPoolFilterManager) WaitForMpoolUpdates(ctx context.Context, ch <-chan api.MpoolUpdate) { + for { + select { + case <-ctx.Done(): + return + case u := <-ch: + m.processUpdate(ctx, u) + } + } +} + +func (m *MemPoolFilterManager) processUpdate(ctx context.Context, u api.MpoolUpdate) { + // only process added messages + if u.Type == api.MpoolRemove { + return + } + + m.mu.Lock() + defer m.mu.Unlock() + + if len(m.filters) == 0 { + return + } + + // TODO: could run this loop in parallel with errgroup if we expect large numbers of filters + for _, f := range m.filters { + f.CollectMessage(ctx, u.Message) + } +} + +func (m *MemPoolFilterManager) Install(ctx context.Context) (*MemPoolFilter, error) { + id, err := uuid.NewRandom() + if err != nil { + return nil, xerrors.Errorf("new uuid: %w", err) + } + + f := &MemPoolFilter{ + id: id.String(), + maxResults: m.MaxFilterResults, + } + + m.mu.Lock() + m.filters[id.String()] = f + m.mu.Unlock() + + return f, nil +} + +func (m *MemPoolFilterManager) Remove(ctx context.Context, id string) error { + m.mu.Lock() + defer m.mu.Unlock() + if _, found := m.filters[id]; !found { + return ErrFilterNotFound + } + delete(m.filters, id) + return nil +} diff --git a/chain/events/filter/store.go b/chain/events/filter/store.go new file mode 100644 index 000000000..2f8a09875 --- /dev/null +++ b/chain/events/filter/store.go @@ -0,0 +1,93 @@ +package filter + +import ( + "context" + "errors" + "sync" + "time" +) + +type Filter interface { + ID() string + LastTaken() time.Time + SetSubChannel(chan<- interface{}) + ClearSubChannel() +} + +type FilterStore interface { + Add(context.Context, Filter) error + Get(context.Context, string) (Filter, error) + Remove(context.Context, string) error + NotTakenSince(when time.Time) []Filter // returns a list of filters that have not had their collected results taken +} + +var ( + ErrFilterAlreadyRegistered = errors.New("filter already registered") + ErrFilterNotFound = errors.New("filter not found") + ErrMaximumNumberOfFilters = errors.New("maximum number of filters registered") +) + +type memFilterStore struct { + max int + mu sync.Mutex + filters map[string]Filter +} + +var _ FilterStore = (*memFilterStore)(nil) + +func NewMemFilterStore(maxFilters int) FilterStore { + return &memFilterStore{ + max: maxFilters, + filters: make(map[string]Filter), + } +} + +func (m *memFilterStore) Add(_ context.Context, f Filter) error { + m.mu.Lock() + defer m.mu.Unlock() + + if len(m.filters) >= m.max { + return ErrMaximumNumberOfFilters + } + + if _, exists := m.filters[f.ID()]; exists { + return ErrFilterAlreadyRegistered + } + m.filters[f.ID()] = f + return nil +} + +func (m *memFilterStore) Get(_ context.Context, id string) (Filter, error) { + m.mu.Lock() + f, found := m.filters[id] + m.mu.Unlock() + if !found { + return nil, ErrFilterNotFound + } + return f, nil +} + +func (m *memFilterStore) Remove(_ context.Context, id string) error { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.filters[id]; !exists { + return ErrFilterNotFound + } + delete(m.filters, id) + return nil +} + +func (m *memFilterStore) NotTakenSince(when time.Time) []Filter { + m.mu.Lock() + defer m.mu.Unlock() + + var res []Filter + for _, f := range m.filters { + if f.LastTaken().Before(when) { + res = append(res, f) + } + } + + return res +} diff --git a/chain/events/filter/tipset.go b/chain/events/filter/tipset.go new file mode 100644 index 000000000..1f43b09a3 --- /dev/null +++ b/chain/events/filter/tipset.go @@ -0,0 +1,128 @@ +package filter + +import ( + "context" + "sync" + "time" + + "github.com/google/uuid" + "golang.org/x/xerrors" + + "github.com/filecoin-project/lotus/chain/types" +) + +type TipSetFilter struct { + id string + maxResults int // maximum number of results to collect, 0 is unlimited + ch chan<- interface{} + + mu sync.Mutex + collected []types.TipSetKey + lastTaken time.Time +} + +var _ Filter = (*TipSetFilter)(nil) + +func (f *TipSetFilter) ID() string { + return f.id +} + +func (f *TipSetFilter) SetSubChannel(ch chan<- interface{}) { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = ch + f.collected = nil +} + +func (f *TipSetFilter) ClearSubChannel() { + f.mu.Lock() + defer f.mu.Unlock() + f.ch = nil +} + +func (f *TipSetFilter) CollectTipSet(ctx context.Context, ts *types.TipSet) { + f.mu.Lock() + defer f.mu.Unlock() + + // if we have a subscription channel then push tipset to it + if f.ch != nil { + f.ch <- ts + return + } + + if f.maxResults > 0 && len(f.collected) == f.maxResults { + copy(f.collected, f.collected[1:]) + f.collected = f.collected[:len(f.collected)-1] + } + f.collected = append(f.collected, ts.Key()) +} + +func (f *TipSetFilter) TakeCollectedTipSets(context.Context) []types.TipSetKey { + f.mu.Lock() + collected := f.collected + f.collected = nil + f.lastTaken = time.Now().UTC() + f.mu.Unlock() + + return collected +} + +func (f *TipSetFilter) LastTaken() time.Time { + f.mu.Lock() + defer f.mu.Unlock() + return f.lastTaken +} + +type TipSetFilterManager struct { + MaxFilterResults int + + mu sync.Mutex // guards mutations to filters + filters map[string]*TipSetFilter +} + +func (m *TipSetFilterManager) Apply(ctx context.Context, from, to *types.TipSet) error { + m.mu.Lock() + defer m.mu.Unlock() + if len(m.filters) == 0 { + return nil + } + + // TODO: could run this loop in parallel with errgroup + for _, f := range m.filters { + f.CollectTipSet(ctx, to) + } + + return nil +} + +func (m *TipSetFilterManager) Revert(ctx context.Context, from, to *types.TipSet) error { + return nil +} + +func (m *TipSetFilterManager) Install(ctx context.Context) (*TipSetFilter, error) { + id, err := uuid.NewRandom() + if err != nil { + return nil, xerrors.Errorf("new uuid: %w", err) + } + + f := &TipSetFilter{ + id: id.String(), + maxResults: m.MaxFilterResults, + } + + m.mu.Lock() + m.filters[id.String()] = f + m.mu.Unlock() + + return f, nil +} + +func (m *TipSetFilterManager) Remove(ctx context.Context, id string) error { + m.mu.Lock() + defer m.mu.Unlock() + if _, found := m.filters[id]; !found { + return ErrFilterNotFound + } + delete(m.filters, id) + return nil +} diff --git a/itests/actor_events_test.go b/itests/actor_events_test.go new file mode 100644 index 000000000..f0f05418e --- /dev/null +++ b/itests/actor_events_test.go @@ -0,0 +1,176 @@ +// stm: #integration +package itests + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + + "github.com/filecoin-project/lotus/chain/store" + "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/itests/kit" +) + +func TestActorEventsMpool(t *testing.T) { + ctx := context.Background() + + kit.QuietMiningLogs() + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs()) + ens.InterconnectAll().BeginMining(10 * time.Millisecond) + + // create a new address where to send funds. + addr, err := client.WalletNew(ctx, types.KTBLS) + require.NoError(t, err) + + // get the existing balance from the default wallet to then split it. + bal, err := client.WalletBalance(ctx, client.DefaultKey.Address) + require.NoError(t, err) + + // install filter + filterID, err := client.EthNewPendingTransactionFilter(ctx) + require.NoError(t, err) + + const iterations = 100 + + // we'll send half our balance (saving the other half for gas), + // in `iterations` increments. + toSend := big.Div(bal, big.NewInt(2)) + each := big.Div(toSend, big.NewInt(iterations)) + + waitAllCh := make(chan struct{}) + go func() { + headChangeCh, err := client.ChainNotify(ctx) + require.NoError(t, err) + <-headChangeCh // skip hccurrent + + count := 0 + for { + select { + case headChanges := <-headChangeCh: + for _, change := range headChanges { + if change.Type == store.HCApply { + msgs, err := client.ChainGetMessagesInTipset(ctx, change.Val.Key()) + require.NoError(t, err) + count += len(msgs) + if count == iterations { + waitAllCh <- struct{}{} + } + } + } + } + } + }() + + var sms []*types.SignedMessage + for i := 0; i < iterations; i++ { + msg := &types.Message{ + From: client.DefaultKey.Address, + To: addr, + Value: each, + } + + sm, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + require.EqualValues(t, i, sm.Message.Nonce) + + sms = append(sms, sm) + } + + select { + case <-waitAllCh: + case <-time.After(time.Minute): + t.Errorf("timeout to wait for pack messages") + } + + // collect filter results + res, err := client.EthGetFilterChanges(ctx, filterID) + require.NoError(t, err) + + // expect to have seen iteration number of mpool messages + require.Equal(t, iterations, len(res.NewTransactionHashes)) +} + +func TestActorEventsTipsets(t *testing.T) { + ctx := context.Background() + + kit.QuietMiningLogs() + + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs()) + ens.InterconnectAll().BeginMining(10 * time.Millisecond) + + // create a new address where to send funds. + addr, err := client.WalletNew(ctx, types.KTBLS) + require.NoError(t, err) + + // get the existing balance from the default wallet to then split it. + bal, err := client.WalletBalance(ctx, client.DefaultKey.Address) + require.NoError(t, err) + + // install filter + filterID, err := client.EthNewBlockFilter(ctx) + require.NoError(t, err) + + const iterations = 100 + + // we'll send half our balance (saving the other half for gas), + // in `iterations` increments. + toSend := big.Div(bal, big.NewInt(2)) + each := big.Div(toSend, big.NewInt(iterations)) + + waitAllCh := make(chan struct{}) + go func() { + headChangeCh, err := client.ChainNotify(ctx) + require.NoError(t, err) + <-headChangeCh // skip hccurrent + + count := 0 + for { + select { + case headChanges := <-headChangeCh: + for _, change := range headChanges { + if change.Type == store.HCApply { + msgs, err := client.ChainGetMessagesInTipset(ctx, change.Val.Key()) + require.NoError(t, err) + count += len(msgs) + if count == iterations { + waitAllCh <- struct{}{} + } + } + } + } + } + }() + + var sms []*types.SignedMessage + for i := 0; i < iterations; i++ { + msg := &types.Message{ + From: client.DefaultKey.Address, + To: addr, + Value: each, + } + + sm, err := client.MpoolPushMessage(ctx, msg, nil) + require.NoError(t, err) + require.EqualValues(t, i, sm.Message.Nonce) + + sms = append(sms, sm) + } + + select { + case <-waitAllCh: + case <-time.After(time.Minute): + t.Errorf("timeout to wait for pack messages") + } + + // collect filter results + res, err := client.EthGetFilterChanges(ctx, filterID) + require.NoError(t, err) + + // expect to have seen iteration number of tipsets + require.Equal(t, iterations, len(res.NewBlockHashes)) +} diff --git a/node/builder_chain.go b/node/builder_chain.go index 3157a354e..3777f33a0 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -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)), ) } diff --git a/node/config/def.go b/node/config/def.go index a6e6fc66a..747b9b284 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -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 diff --git a/node/config/types.go b/node/config/types.go index dbfa2e432..0032930fd 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -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 +} diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 9b5472e37..a06c5448a 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -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 + } +} diff --git a/node/modules/actorevent.go b/node/modules/actorevent.go new file mode 100644 index 000000000..7e64470f4 --- /dev/null +++ b/node/modules/actorevent.go @@ -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 + } +} From 7383ecb0ba32975800c9cd5bc15156535e7c4481 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Thu, 10 Nov 2022 15:04:58 +0000 Subject: [PATCH 2/4] make gen --- api/mocks/mock_full.go | 135 ++++++++++++++ api/proxy_gen.go | 129 +++++++++++++- build/openrpc/full.json.gz | Bin 31634 -> 32530 bytes build/openrpc/gateway.json.gz | Bin 5172 -> 5167 bytes build/openrpc/miner.json.gz | Bin 16049 -> 16052 bytes build/openrpc/worker.json.gz | Bin 5260 -> 5259 bytes documentation/en/api-v1-unstable-methods.md | 184 ++++++++++++++++++++ node/config/doc_gen.go | 47 +++++ 8 files changed, 489 insertions(+), 6 deletions(-) diff --git a/api/mocks/mock_full.go b/api/mocks/mock_full.go index 2e157a9c6..7801497f4 100644 --- a/api/mocks/mock_full.go +++ b/api/mocks/mock_full.go @@ -1116,6 +1116,51 @@ func (mr *MockFullNodeMockRecorder) EthGetCode(arg0, arg1, arg2 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetCode", reflect.TypeOf((*MockFullNode)(nil).EthGetCode), arg0, arg1, arg2) } +// EthGetFilterChanges mocks base method. +func (m *MockFullNode) EthGetFilterChanges(arg0 context.Context, arg1 api.EthFilterID) (*api.EthFilterResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthGetFilterChanges", arg0, arg1) + ret0, _ := ret[0].(*api.EthFilterResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthGetFilterChanges indicates an expected call of EthGetFilterChanges. +func (mr *MockFullNodeMockRecorder) EthGetFilterChanges(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetFilterChanges", reflect.TypeOf((*MockFullNode)(nil).EthGetFilterChanges), arg0, arg1) +} + +// EthGetFilterLogs mocks base method. +func (m *MockFullNode) EthGetFilterLogs(arg0 context.Context, arg1 api.EthFilterID) (*api.EthFilterResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthGetFilterLogs", arg0, arg1) + ret0, _ := ret[0].(*api.EthFilterResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthGetFilterLogs indicates an expected call of EthGetFilterLogs. +func (mr *MockFullNodeMockRecorder) EthGetFilterLogs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetFilterLogs", reflect.TypeOf((*MockFullNode)(nil).EthGetFilterLogs), arg0, arg1) +} + +// EthGetLogs mocks base method. +func (m *MockFullNode) EthGetLogs(arg0 context.Context, arg1 *api.EthFilterSpec) (*api.EthFilterResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthGetLogs", arg0, arg1) + ret0, _ := ret[0].(*api.EthFilterResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthGetLogs indicates an expected call of EthGetLogs. +func (mr *MockFullNodeMockRecorder) EthGetLogs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthGetLogs", reflect.TypeOf((*MockFullNode)(nil).EthGetLogs), arg0, arg1) +} + // EthGetStorageAt mocks base method. func (m *MockFullNode) EthGetStorageAt(arg0 context.Context, arg1 api.EthAddress, arg2 api.EthBytes, arg3 string) (api.EthBytes, error) { m.ctrl.T.Helper() @@ -1221,6 +1266,51 @@ func (mr *MockFullNodeMockRecorder) EthMaxPriorityFeePerGas(arg0 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthMaxPriorityFeePerGas", reflect.TypeOf((*MockFullNode)(nil).EthMaxPriorityFeePerGas), arg0) } +// EthNewBlockFilter mocks base method. +func (m *MockFullNode) EthNewBlockFilter(arg0 context.Context) (api.EthFilterID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthNewBlockFilter", arg0) + ret0, _ := ret[0].(api.EthFilterID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthNewBlockFilter indicates an expected call of EthNewBlockFilter. +func (mr *MockFullNodeMockRecorder) EthNewBlockFilter(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthNewBlockFilter", reflect.TypeOf((*MockFullNode)(nil).EthNewBlockFilter), arg0) +} + +// EthNewFilter mocks base method. +func (m *MockFullNode) EthNewFilter(arg0 context.Context, arg1 *api.EthFilterSpec) (api.EthFilterID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthNewFilter", arg0, arg1) + ret0, _ := ret[0].(api.EthFilterID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthNewFilter indicates an expected call of EthNewFilter. +func (mr *MockFullNodeMockRecorder) EthNewFilter(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthNewFilter", reflect.TypeOf((*MockFullNode)(nil).EthNewFilter), arg0, arg1) +} + +// EthNewPendingTransactionFilter mocks base method. +func (m *MockFullNode) EthNewPendingTransactionFilter(arg0 context.Context) (api.EthFilterID, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthNewPendingTransactionFilter", arg0) + ret0, _ := ret[0].(api.EthFilterID) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthNewPendingTransactionFilter indicates an expected call of EthNewPendingTransactionFilter. +func (mr *MockFullNodeMockRecorder) EthNewPendingTransactionFilter(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthNewPendingTransactionFilter", reflect.TypeOf((*MockFullNode)(nil).EthNewPendingTransactionFilter), arg0) +} + // EthProtocolVersion mocks base method. func (m *MockFullNode) EthProtocolVersion(arg0 context.Context) (api.EthUint64, error) { m.ctrl.T.Helper() @@ -1251,6 +1341,51 @@ func (mr *MockFullNodeMockRecorder) EthSendRawTransaction(arg0, arg1 interface{} return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthSendRawTransaction", reflect.TypeOf((*MockFullNode)(nil).EthSendRawTransaction), arg0, arg1) } +// EthSubscribe mocks base method. +func (m *MockFullNode) EthSubscribe(arg0 context.Context, arg1 []string, arg2 api.EthSubscriptionParams) (<-chan api.EthSubscriptionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthSubscribe", arg0, arg1, arg2) + ret0, _ := ret[0].(<-chan api.EthSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthSubscribe indicates an expected call of EthSubscribe. +func (mr *MockFullNodeMockRecorder) EthSubscribe(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthSubscribe", reflect.TypeOf((*MockFullNode)(nil).EthSubscribe), arg0, arg1, arg2) +} + +// EthUninstallFilter mocks base method. +func (m *MockFullNode) EthUninstallFilter(arg0 context.Context, arg1 api.EthFilterID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthUninstallFilter", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthUninstallFilter indicates an expected call of EthUninstallFilter. +func (mr *MockFullNodeMockRecorder) EthUninstallFilter(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthUninstallFilter", reflect.TypeOf((*MockFullNode)(nil).EthUninstallFilter), arg0, arg1) +} + +// EthUnsubscribe mocks base method. +func (m *MockFullNode) EthUnsubscribe(arg0 context.Context, arg1 api.EthSubscriptionID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EthUnsubscribe", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EthUnsubscribe indicates an expected call of EthUnsubscribe. +func (mr *MockFullNodeMockRecorder) EthUnsubscribe(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EthUnsubscribe", reflect.TypeOf((*MockFullNode)(nil).EthUnsubscribe), arg0, arg1) +} + // GasEstimateFeeCap mocks base method. func (m *MockFullNode) GasEstimateFeeCap(arg0 context.Context, arg1 *types.Message, arg2 int64, arg3 types.TipSetKey) (big.Int, error) { m.ctrl.T.Helper() diff --git a/api/proxy_gen.go b/api/proxy_gen.go index a6701a71f..2677c48bb 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -33,7 +33,7 @@ import ( "github.com/filecoin-project/go-state-types/proof" apitypes "github.com/filecoin-project/lotus/api/types" - "github.com/filecoin-project/lotus/chain/actors/builtin" + builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/journal/alerting" @@ -245,6 +245,12 @@ type FullNodeStruct struct { EthGetCode func(p0 context.Context, p1 EthAddress, p2 string) (EthBytes, error) `perm:"read"` + EthGetFilterChanges func(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) `perm:"write"` + + EthGetFilterLogs func(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) `perm:"write"` + + EthGetLogs func(p0 context.Context, p1 *EthFilterSpec) (*EthFilterResult, error) `perm:"read"` + EthGetStorageAt func(p0 context.Context, p1 EthAddress, p2 EthBytes, p3 string) (EthBytes, error) `perm:"read"` EthGetTransactionByBlockHashAndIndex func(p0 context.Context, p1 EthHash, p2 EthUint64) (EthTx, error) `perm:"read"` @@ -259,10 +265,22 @@ type FullNodeStruct struct { EthMaxPriorityFeePerGas func(p0 context.Context) (EthBigInt, error) `perm:"read"` + EthNewBlockFilter func(p0 context.Context) (EthFilterID, error) `perm:"write"` + + EthNewFilter func(p0 context.Context, p1 *EthFilterSpec) (EthFilterID, error) `perm:"write"` + + EthNewPendingTransactionFilter func(p0 context.Context) (EthFilterID, error) `perm:"write"` + EthProtocolVersion func(p0 context.Context) (EthUint64, error) `perm:"read"` EthSendRawTransaction func(p0 context.Context, p1 EthBytes) (EthHash, error) `perm:"read"` + EthSubscribe func(p0 context.Context, p1 []string, p2 EthSubscriptionParams) (<-chan EthSubscriptionResponse, error) `perm:"write"` + + EthUninstallFilter func(p0 context.Context, p1 EthFilterID) (bool, error) `perm:"write"` + + EthUnsubscribe func(p0 context.Context, p1 EthSubscriptionID) (bool, error) `perm:"write"` + GasEstimateFeeCap func(p0 context.Context, p1 *types.Message, p2 int64, p3 types.TipSetKey) (types.BigInt, error) `perm:"read"` GasEstimateGasLimit func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (int64, error) `perm:"read"` @@ -732,7 +750,7 @@ type StorageMinerStruct struct { ComputeDataCid func(p0 context.Context, p1 abi.UnpaddedPieceSize, p2 storiface.Data) (abi.PieceInfo, error) `perm:"admin"` - ComputeProof func(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) `perm:"read"` + ComputeProof func(p0 context.Context, p1 []builtinactors.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtinactors.PoStProof, error) `perm:"read"` ComputeWindowPoSt func(p0 context.Context, p1 uint64, p2 types.TipSetKey) ([]miner.SubmitWindowedPoStParams, error) `perm:"admin"` @@ -1983,6 +2001,39 @@ func (s *FullNodeStub) EthGetCode(p0 context.Context, p1 EthAddress, p2 string) return *new(EthBytes), ErrNotSupported } +func (s *FullNodeStruct) EthGetFilterChanges(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) { + if s.Internal.EthGetFilterChanges == nil { + return nil, ErrNotSupported + } + return s.Internal.EthGetFilterChanges(p0, p1) +} + +func (s *FullNodeStub) EthGetFilterChanges(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) { + return nil, ErrNotSupported +} + +func (s *FullNodeStruct) EthGetFilterLogs(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) { + if s.Internal.EthGetFilterLogs == nil { + return nil, ErrNotSupported + } + return s.Internal.EthGetFilterLogs(p0, p1) +} + +func (s *FullNodeStub) EthGetFilterLogs(p0 context.Context, p1 EthFilterID) (*EthFilterResult, error) { + return nil, ErrNotSupported +} + +func (s *FullNodeStruct) EthGetLogs(p0 context.Context, p1 *EthFilterSpec) (*EthFilterResult, error) { + if s.Internal.EthGetLogs == nil { + return nil, ErrNotSupported + } + return s.Internal.EthGetLogs(p0, p1) +} + +func (s *FullNodeStub) EthGetLogs(p0 context.Context, p1 *EthFilterSpec) (*EthFilterResult, error) { + return nil, ErrNotSupported +} + func (s *FullNodeStruct) EthGetStorageAt(p0 context.Context, p1 EthAddress, p2 EthBytes, p3 string) (EthBytes, error) { if s.Internal.EthGetStorageAt == nil { return *new(EthBytes), ErrNotSupported @@ -2060,6 +2111,39 @@ func (s *FullNodeStub) EthMaxPriorityFeePerGas(p0 context.Context) (EthBigInt, e return *new(EthBigInt), ErrNotSupported } +func (s *FullNodeStruct) EthNewBlockFilter(p0 context.Context) (EthFilterID, error) { + if s.Internal.EthNewBlockFilter == nil { + return *new(EthFilterID), ErrNotSupported + } + return s.Internal.EthNewBlockFilter(p0) +} + +func (s *FullNodeStub) EthNewBlockFilter(p0 context.Context) (EthFilterID, error) { + return *new(EthFilterID), ErrNotSupported +} + +func (s *FullNodeStruct) EthNewFilter(p0 context.Context, p1 *EthFilterSpec) (EthFilterID, error) { + if s.Internal.EthNewFilter == nil { + return *new(EthFilterID), ErrNotSupported + } + return s.Internal.EthNewFilter(p0, p1) +} + +func (s *FullNodeStub) EthNewFilter(p0 context.Context, p1 *EthFilterSpec) (EthFilterID, error) { + return *new(EthFilterID), ErrNotSupported +} + +func (s *FullNodeStruct) EthNewPendingTransactionFilter(p0 context.Context) (EthFilterID, error) { + if s.Internal.EthNewPendingTransactionFilter == nil { + return *new(EthFilterID), ErrNotSupported + } + return s.Internal.EthNewPendingTransactionFilter(p0) +} + +func (s *FullNodeStub) EthNewPendingTransactionFilter(p0 context.Context) (EthFilterID, error) { + return *new(EthFilterID), ErrNotSupported +} + func (s *FullNodeStruct) EthProtocolVersion(p0 context.Context) (EthUint64, error) { if s.Internal.EthProtocolVersion == nil { return *new(EthUint64), ErrNotSupported @@ -2082,6 +2166,39 @@ func (s *FullNodeStub) EthSendRawTransaction(p0 context.Context, p1 EthBytes) (E return *new(EthHash), ErrNotSupported } +func (s *FullNodeStruct) EthSubscribe(p0 context.Context, p1 []string, p2 EthSubscriptionParams) (<-chan EthSubscriptionResponse, error) { + if s.Internal.EthSubscribe == nil { + return nil, ErrNotSupported + } + return s.Internal.EthSubscribe(p0, p1, p2) +} + +func (s *FullNodeStub) EthSubscribe(p0 context.Context, p1 []string, p2 EthSubscriptionParams) (<-chan EthSubscriptionResponse, error) { + return nil, ErrNotSupported +} + +func (s *FullNodeStruct) EthUninstallFilter(p0 context.Context, p1 EthFilterID) (bool, error) { + if s.Internal.EthUninstallFilter == nil { + return false, ErrNotSupported + } + return s.Internal.EthUninstallFilter(p0, p1) +} + +func (s *FullNodeStub) EthUninstallFilter(p0 context.Context, p1 EthFilterID) (bool, error) { + return false, ErrNotSupported +} + +func (s *FullNodeStruct) EthUnsubscribe(p0 context.Context, p1 EthSubscriptionID) (bool, error) { + if s.Internal.EthUnsubscribe == nil { + return false, ErrNotSupported + } + return s.Internal.EthUnsubscribe(p0, p1) +} + +func (s *FullNodeStub) EthUnsubscribe(p0 context.Context, p1 EthSubscriptionID) (bool, error) { + return false, ErrNotSupported +} + func (s *FullNodeStruct) GasEstimateFeeCap(p0 context.Context, p1 *types.Message, p2 int64, p3 types.TipSetKey) (types.BigInt, error) { if s.Internal.GasEstimateFeeCap == nil { return *new(types.BigInt), ErrNotSupported @@ -4469,15 +4586,15 @@ func (s *StorageMinerStub) ComputeDataCid(p0 context.Context, p1 abi.UnpaddedPie return *new(abi.PieceInfo), ErrNotSupported } -func (s *StorageMinerStruct) ComputeProof(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) { +func (s *StorageMinerStruct) ComputeProof(p0 context.Context, p1 []builtinactors.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtinactors.PoStProof, error) { if s.Internal.ComputeProof == nil { - return *new([]builtin.PoStProof), ErrNotSupported + return *new([]builtinactors.PoStProof), ErrNotSupported } return s.Internal.ComputeProof(p0, p1, p2, p3, p4) } -func (s *StorageMinerStub) ComputeProof(p0 context.Context, p1 []builtin.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtin.PoStProof, error) { - return *new([]builtin.PoStProof), ErrNotSupported +func (s *StorageMinerStub) ComputeProof(p0 context.Context, p1 []builtinactors.ExtendedSectorInfo, p2 abi.PoStRandomness, p3 abi.ChainEpoch, p4 abinetwork.Version) ([]builtinactors.PoStProof, error) { + return *new([]builtinactors.PoStProof), ErrNotSupported } func (s *StorageMinerStruct) ComputeWindowPoSt(p0 context.Context, p1 uint64, p2 types.TipSetKey) ([]miner.SubmitWindowedPoStParams, error) { diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index d32810acc56567c249cc958f6b6b437426ff0d0b..354f27028d8f2ef999849ac2f12275b1599c8cce 100644 GIT binary patch literal 32530 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%0DG=L?9QO7s8{6&Q24U`Nojbk;ftw9 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 R{~rJV|NkqLh*acy1^^k4Y9Rms literal 31634 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�ptb23AYDNHgCCYqD%j`G{M2LlqlSr&?BLr~PVz ze+Xg$75~v`by_dm?U$X-$JSoI)9Lp*FI&6)Rx4dh>zkRY(G_s33weO}K`M=5=S6w4 zg5WHLT!n|sj=LopY^TsW@i?AAj-r&zQ>dL6T?nL1*}8g92d zX0C7#sBl6X@eRwC8i&k-sc)0*(A@NFe{Om6fgq5qgc#yVv3e8R{*nfRJyZ zgweny-p|GeUSJ+9um)dA42&4t6fMPf6w%xxSNl@ zNJOvID!FJKHa}1q0AS|4qG$DtKiy{gAu$qAhJ`smnSI0hsVyN)3=CNRQEL~?no|cNg zlAZHYOMmFKIwf}!ha7TrV3V>6E*5Udr{6*bKSm(}uCa~S5p^8M5rfVlllBT|fAqSf zI6^2OPZ2+`IMu$-oa(%u5gz5?hLBcjlX%FD+=k39qOr9bTl?Pc78SE!=A}|qr0C-+ zoB5<#SPyGs(4^G&bhsxb>puAEw_05b&0M|+|BH0>!xEIPj;I|J%6`s9?L#;*X7ze0 zr)EDk^|f14i6a!=@3F_vF+r@Jf8UgJSN|%(mv6dJqhyrI#HqF8Myb_o+ijahGw7rt zt%}1GwNSw4(lAiZ7JBU2EN@CGiwKUQ^vZY7j_V1RsPp}XsN+%ZGC(b@tTCAzleq~` zKRBtkS8_KwWbbpxt`M(zPbUvy%}fj#)Jo}>YF(9wA-B^PfZ6pmQm~PNfA^S$QOTd# zw<@876VGDwCUVK&Q|k)Zbt1Ge7+pU)9`R|%Y4=MmP)ZVK$SH%9@KkFJxhz4cIvfn% zpBHjiOMNM%`TSwrD|HDU6r+yt8w>*S$MB$7@8hMoc$hi-JmN@;eo=9L$t@ z;!CCBL2zMNjk+<}9ye{OXft57%ltA!H97A#!`HoEOVk~P1pmRvkg^=uxN>*dE&$@%iw5u2Of0k%e>6X}%Hnpc% zKzDbYBU;P`HOiYvT)X_#ahyd_T+<|G8VwrFvysvob6ly^OX5tG_8GRl&$=mFYE8CV zB{$H8*@OvVXJkz4S&?z?H9h+n%xVKNZdv8g$8BnO^U`{@(s*n<`A2Flmh++fM#WV8 za6{52*=NeG(oZhXNKO7>z_yG!5ou*fN3<7T`%fE61BBR_4f>$nIqv>R>F3j*^PhkI z_dnO@-M_hex_jWUPyaf!KF{ABz8!Ync_;MrZI z?po9>XGRARp2g^vRaLb7#X(8Th@h4R@5xZ=ejG-u)k6z$E5&SpQvSt zt1@<#AlB@kNPeY`H3_jm%{{q1t9HAjtVd!Z59p2lhkuVU-PcSNP5`Mn4Pny_hMVPe zG_FG9D%@vexts;l?-Va%#2xy-z$LsnTyPZq4lT~VeCS}lIIw>tPs2J&96B}ny@TF4 zCefqN4%IV5Q~irnYq_`=01dHJ5ws}Z!W(vvtwmIH+gnwpQMCV!Wfs1y|u_)%(_+6$E)DdZ}5C_sct z--rTKnhFY0I`EW9P02+r*=ha0F$pR6%u%d*&oo6pJ>R@rRZHgr@f*sn;?G>vu{zQq z)bBzqzpAQsK%73NeVBZ|7wuPMxjlgo=l9yXRmES12r_GW?rDFBB;8xlUJM2J@>`T{H@swU zG=-RiE}Ln%y|DOLc+TTZ#3j>}GJQFV&40;sd0}#euwaIb$+-Q(wNfKB->&$1F z)|M6rcUbSP-D+>(qD=2M6}&4x9E{~IHI^ge}|Q~M@3xemDNl2X2@Io-yCA0sO;V@x1~=*IYCQ{vGC?+~6$dA(3;+&Ne6 zTRh8M*E6!D-POtMOYN!NhzSuXemeJ)at>ZhlC^TNmoy0aW34c`(K5bW7kR%>PIjuU%4f?lW|p+7;zfofU&r&y8myPH~7QXM|U= zW(s^W1%t})v{*Dd|82W@vUTC7BFQ3Sza%8JeA&cLvV7Uu1-G}~ZvPhm0RR7?LzEa6 G_y7QjWL<9n delta 4300 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 diff --git a/build/openrpc/miner.json.gz b/build/openrpc/miner.json.gz index ceff0c98bc930fb0a1506f09deb152e3d904fe75..b51a00d412b07e661951bcb2b26bbbcab1d78ee6 100644 GIT binary patch literal 16052 zcmV;lK1;zLiwFP!00000|LnbcQyV$fIQ&&q{nlHXOc~-kHdXrvfh5BYnc)F4^E?}< z9l2Y!wX~&9w+uKbzWc8v_5G^87#!%`sTtg@K01=lO-JV({m`g|h;8ZGq1M~)?~k>P zhRK|2hd(rinTfPR?ToUu11`>w!O{7t*3s^e?O;m87rp6zch5j~+G9ssA-1H3qaA+z zp%J*^pMJz9(kUkUM>?bSkzv@#akP$RAV;^c#Uz-&|NeVHugQE(bnwOjzg#kE!v*?` z39>Jlz1G=V5C-tZ>H?h-h921dvHb5_@CM44`);ESK(dZ%m-|&l7jLZY+iUW8 zP5%D-Z>^&_>(vU{8|_fjI+_J-xRQfh+kDDqn>~v;k$PW(PUqzt9Nsk-u5h;a=hM|oOa*MOgekFf(}FWJ80o| zmQ5cv|5+fizo7r$*>KeBYmbkee5yUBWR4fLX;*?tr{G8vRr-~!bEr7dt}+e#cyEQ0 z^XEAJwwQ{4Xy-OMrmGcZe0ptaMoBy8)fKW=m_UY7-<}|7p08&nc9!Yyu>)r&`s1D; z`y({X8Psn*esJIYMeW=8%f}*ivqF!4#L@quVTM-XjRMblEc@s`fVK@c{CC>unggCX zH9x#|GfGVaNvLIfqSBfsm$@-~lZ4O^=*Jf(J^O3NC7zwvieU33O8Qsia_SUnKe z)Zh)A;r-ydLSY&-&9et0Fw2Y5$T&FOwPmS4=~i#|wSTG!6PeQF7~-!phTxg1WWAap zI|1f*@fctSK;SnC4SO&gjHU;J$zVLW2?po3{JEKPFw9}jzc5=GHoSiiO-S?_92x_Z z$)+>Be?@N*Ia<**;U#55O?vMDM!~)N2F%=7 zbDOTfiq9B;yc^z#-ix1-F|Wy`g>*dM04Qb!KnGlF-D@oZZoW2659U6&T4D#t{T7hJ z;iSDnjsq769G{+C6G#kTP~-qY8Mud-fjPDPCg=g_Yre|Yq$zO|U0XGA(|tNg^syE9Nas`a=aP9!I;ek&MjJ5|D_M#to4SjtpCnd zbF`TchI2evZ^r928usso+G9v+I?Tpo@smr<;V4~dzDESjv5uj=X+m~pgKJcRW_Q!D z9L*h1wlvMb&l9Xm1ZF`iKM#_1yVc!~7ebDQ45nORM$ zMj+$~(hIb}96w~7(@S=4Q#$Ves}H{3IE>nO4t3=2J}wY{GXOJ6&9~sq3R+)-t%=-S z+{jhQlfx0jazt+q_)_9HJ`sH2e1Lo?+ueGmPwv+CZ`F5g!6^EPUQ@|HN$7t4p`D)a zXM!%Z!(O-B?YN`R4yVFzMd|!XpgeiJx#@VGrLP69rKlfJc2eR47oU^~cU@HO@5cb! z;2AHeyK*d)-05qls^irCMsj=q-6J z7VbWGPZ1Mx9d{7bRMpQc4i6@|;?OhODhopaAWIZ}y_v1C$uJR{bZ7s=14G0>hSYpg z=vjRsF}V$S0f|9ZM8tMj1M&EH2{HL#QGM9~){s9Uh}~2BcHiCBy#;ULy@YK2LWwHc z<;c@a*i~e!H&Q{8NjVeDLbXpTnB|RH)k)=+WKT86)cgB?{!@#rSQYi1P}bYnI7CIw z7VVE-x7UCHGE5slOfYV55S2IMY353%fGrec3SNh({0ctc@=N^ne#7`%?f;0`{rBbj zrB42}ME~>8e`?i5O{fbvhqgin7MRa4f%fJD(W$|u1DBl+8V^IUCVBg}sw}dj zHq2z2Ys377i9zrAyw@>g0w`J5%o-Cm9tw+v&#tq7g{@1(enT5!goxva{mb=ig_#8A zdd7s7R&$+k~U<<>;hu)mdEUToD>N)57#IUO9I$VsZ}qYD@UXP+^yM9rQtfGuii+qoMZ6b&pfuYj z2pT3PNIM+z{VKGX7_FqpQ(~Y8`R4K#TNWR~*HNb@M5YkMZg6QSRngIe?I7b6r3G;) zRDI+HH=ndaX%$shFUg)%v`hkxP^6=Z4OZAeN}#E`3Q9^av`VTmm@!AWTv#RJ-dp%` zL7r(&@c5`b3aU1SXR{ni1+u;@+hW}9X4n=sf(*Tf`t91PW>$m%WLXsMk)JdTQZ0iJ zkaVMFVE1x5zo(+~J0?t`l(x+Z>Lg&+(E zuq9;T6R|9NGIj$yz+MxA$wCBa@b}l`?2oH2m;ZTxbo|?w^Z&d*y8Q5;^P{U@0U~$U zrewwU(Ra|s;)vBj3@{2Dx^YzASak+%HaUs?rZvzkj%(eVepq4WCBY3Cteh3qLO&qX*_6#l%(5Y!6 zozD@Vl1EOCK8XV@v|oxz(rv|*k);@j>8+HkLgA3pW^e(=2o4e)J*XwR~ zpo=9If1(;To(nT^RNK)!zhc65=wX#ip?L*5yq}TX=h4e4!3;waf0vX~i|1poDr@!( zcy*P0Hk#S`J_{A>|8@W*U`>Jt25@+j{cCbf{Li2e4>~0dc9;X`9Ly2Zmt2<#TY-P^ z4+j{KLGZ>v&;%CTm=qdsMLeg}*p8fg;H+oP#$jmH0SiQs4H<_63BcUamwMhpHt)*o zK)ksojDk7Tv56VvBnKI?fsL4r(H%4$@#Q-irEkehIR2+c#lWF!TStxD_zts;KprvQ z@Um=rhsk0eoDzp2Xt>Y?@DSi@A}pvmwjBnb5IM+*M$*nHpT6*nc!VKBdL`J|=9PrNo%g)IMeOvRziLE_v=u#94 z$OburaD0kbqLoh*<|d6A<`A0>!1I8%If`;6fMB3Yj>+%n9yt7u{cCc{z|5rjtpnDC zVKY8Kb4;+a6tve`ojGrN!xfwc<$Am#?ZJBUrOg``C6ZG)XC=jRo) zZxM3>E!4P?n;M;y-sXu7TrRA%LoEqlDPJ4CoS*PKdkQ*E9x z)uxr}tz6%dTpwp-OL>HIfpmI@?D`IjFreDri!iXJTo*9}i-mLq3CI{)Sxltr+wui~ zVzU-;T2$N235w=$GB$K>hnrw42Ttdqrvi%(-|%|6L>Q-Hn%iC9&&|LG8F?oGl}hhV z6C50Z)Bp>Ipf$(CQ502}IE+TpRkng;4NY?+{Z)qhmHZ{mEX&OeX!+=;=A%hQ!omrH zrgKb*gAHW=F`t{5pbO8nCtg-#szR{o-jx}pX6_EB5N5>*^0nTf!YMwrhwfF z-KL}867JIwZa*u$C%Hxq3)=+D#)x+7qBafnmSCTTUR058y(w|!)-!!NV5N{gTZIEsT`Yqu;4dITmVlwzToUZs< zx|KUBl4$^=;IP!K1XJXli^8n!mrJ1dBx!!_VdZny1gj} zs|EG0+~P`}jUZ5TiTl{Zh%oKYE8H*g{_eO%5q@?CXULRexn$6eC82&GM)ZM8Mdvow z(K)i+ms&Rj{g|2}bedx#UPwe@kHcs7ZW;e%_h-;yUo3+&V+;)wI?L!IZwii0h*#QS zw?@sE@Zyz8uyrTA>=hu%`6&w_SSFS9Pj|`7c2d29R)|~`(^FkCEsbId36_NsCPSs9 z;8cz%+iR48b8>N_m`eUBL8D2IQmbkdXHccDLQJaCKE}aSs*Aq-aG!&NtZ*N$B_)qy z+8!k~4=;?d zCs|=YoT@Jo+foEW5#%hVgsgX-?}jLX-H3BSVE|jdQ0w{1N58tA@H8*VCrJyH$}#gvbC=S3YFpmM$5x(2%7@ zoKT%I+pSh43Xee3lqKB|FnIg=Rf=96r5=*-QeFJA zbyyk-9+gHZR8+%(fS`LpoZuchz(zX4cgW~m6Gv3$+5jCAV96};IK_8dXCkGGN1Uld4O(yfe(ia;kc9vI^h9tMWlLm>S!qlvWlaCp zF2osZHEX?-~Ro7cj(vuW!9(hkz;@P*L(f1hhN`+yzKw# ze4wAM9_-I&cfb8Fn^%rD-_cv5U!ClmW<@OTLcL8jZ<&nl2lWf-_D;Lb$-zeD@=6|9GAhPkD4DA9ny}60~s#8 zcC^D@M|%&QW)m__ozPUsi8^&IksO!&{U=@POEJvYgnvek0~aYP@dvja&QYDI@uomf zs<^w1!G(LVt^3#4AKLGdI%D0Rq@;cMFXa_D#uXM{cDvo)p7`gg+dUNj{9Eg2ZlHO7 zqcggYSe(&CvL6Kyk2jAukB=QK%r1%CGtQ2DJzCyqd86fxIB%4^OFvD`RJ*t?!ZBqo z?K@FRDG58o1PhP6De~A~w4;VahZj0Tl5_p4N zRUg}}q!bs7H|Z)YY_H-q#Y=o$@_67j1Y!pq#lM+Lhp=en%4(w$g6UFS&Ddhg#+;5*Jk!Nf@0C zr7t3Ls`y2<${1oHOBchCE2E@0f>kb4?4c53sX8%CI3*TXl2)Z_@H;MnHdU$OXVwyf zjHD`GZ2Bp6wbex!Qik5}Z|-y@qpGq9gKk!9#E{;O`bZgeQfH($Qc)CMjhpuy<|&0Q z#6g_N0yS|CH!DP#n1MTJO4n-a;VIE=1RW$~emM!A0IyV0D64z5azx=#mzAKA392aN zUWkYoWn|1q9&u|-B4}1Fr-rjcs+!os&%Cj6@~Lqi5KO z#KhDIGfKe9zJWPt@ag zTpqMNJZKLO+QWk0Kw7p{d- z4y~xwq7-HxRjS+Alm|iY%AWp+5whbcL$APLFwHF)#W%qXT=|d%EX!RGk2v(CEdNN5 zoSNf6<>Cj|Z&27{MGrz^yjyZ06yl%aN>KSm&8Yx4CWS_`;SPL{!$p`Wgct{W0q?W6 z-oByW?y{0`r)8r|(;`(lX(QeXf4#%C8^m;+k~!Uumq*G0zW0i5My0ypzo#ATkNMm| z%<}`CA+lgg`Rnl}bf(aXNj5$qO1rk`l$YL;DULM0E@gvWqJpZVS@`sEh?-WjXWWAvWu;u&qGKKash4wOqClQ9j+~O=3h}j!g7gfHkDjihy3#-!MH6`8>SZ@95#kS-^ zdz;kr3V~6^-QJQXF6-9uyUeQNCm5d3qqF}WFHQ*)#!l?P0KggEKVCvi#9prhtf6>x z4h;ht{$95OtWnzYLP>I&QwgxF{P2ewwum^!9J{Sbk?^dk>HDr$ zsVIWtDt{F;LJZ-p8beLE z$N7ZV@##rwpnwYH#(@;NSl;|%iBA}{(Z2gp9!1AHXoRj)ux?#0ug&F>LWoHLl-fo~ z3^g7UBZS!*)kUtPcU_?A1u$}ZD3a^jX*@Wm^v!J50}0||trnV6GIgqY@Iz=Fng|Vj zX{YSdjJhGd7!E>;Yv~{#OgSy&f2O2|{MOBCqA>2QE_%(nD4Q9wS}EVxn1|jR@pO4&4h>VF`8fveHL-y^QpcfTHDJHQ8AH)d&;IzqZK2KCUeTw+y`V z4BXEMr#?54u|U%Psm8n^0O^biXL#Q;uyT1f1SyAa!{ExXZWPmWw!K0t3qT8Vnga|u z43kBvFw!(;4x^lUupN4-vSV<|P4&9nZb$Qrre()l2o^ddV1~e&OL#_sfpinv2-va> zMh;j(vWBL)5mMhl#Bjh$CiOelgi#UkEeb{gC`5z^12+(Z*mMA$d!Ud395H2%kn<9M zM|^#JdcqreP(?>eN_bPYMAu{mAI_o81P3|i$aV*?acc{JxlLCBkWR@Qmy-*5PM_pL ztLgS$f6(dmhv~|>OzcY{(xsyIYb}}9lG(OM9%aO+UkFA&GGu&MMWGx9ktLQLMr}NY zI@&*mrs?M7i14()TWt-%FVS`%q_EBmI*RsPH`*vMlH#14hGbcMb`(vTOTUmshH)dS z#c16F`TxC?e|i7cN?JnFPv`qCRpe!%$+UtGrxIXq+|gE;gnq@ng_l)|cx=~vtADp> zUfgelSyTxmRCu8IX_ZeSRZlz{nDt~FbDBU>Ju;^T5=FZ*>S>ideHP%9ZZXfS z>eL3y)zG2ij1&*4HclXeY4)1B3UwhohX!@KZbpFy<0#gj0kVHl`<8DWIE2Rb2(4ai z^=hkETfN%q)g9KW2N`)Ut~R0f#H7%u;cZU?R7%TM)2WgE8d9zCF+9nI_N^9ewP=wR zZRL3@&s}*w$w(@aBJ3k#A|JS_7#H7fUwVsfni1L_#;vSwWpyj7TUotJvU-}4Oy^22 zOrVLsqYGr2Scmeq+d8s44c-&T^0aOnlIswjh%K9CW_5E%}+FDeS)%LSQ z0X>~`Z_>|_7(*-1TY28f^H!ekl05HaiT}wX(mJ{jKb8W&bY8{(eSkp_FEy=^b*VcnxVEg0g-29_p+ai5|timEo-n zZ)JEZ!*@xB4>Gb#rTjX#>0Q*VoD?|fvOEP^DVZKTbb+3GX}oi@x&{Z1FkNv^1))B5#vngk-) zuSt#8-wWShqzZU2NP z)?rghB|eJwq^DIYJE2xuncm8DSEf&LGn|{suBe_>k+E<8W6kR99vV-!$V#$kcM+|) ze|B-7%|nXR@;kb}ltx`+S6cjz?tM$~d8(5{234*+NeuZXxe=n>qqX{6L7z`^Gu%1B zAJf$evpNwQAt)7fx%+i3Egr(6866%SR;$5V4ZedKJm8%6)T-5DFI0;Sy1BXkfuODv zqY;8qQH#|K#|RM;j zP|Yc`$oN`)*6OpJ)Mu#OleK#7H^W1~j?R}_&gD7AKz z)+)6fRcZnMDAGyOXL)?~pYd$4>cL=pD%k4oUDVx!+?pJcYU}0!grICcVl&iPGnzSy zeH*FSs^pze$*l};WwxSEpc}aes9JqwrzJ1g)L-af*QNa)t@W1^B*Ol zv=!}KEN04S?v`!OMuSO4T5OmwPcU56VObwY7Q4f!iGjQH)aw{7PG+_b~}}lH&;c*f!0@$DQM;~S^@zb5-{Wc zF->FuN`NEUg!a8+yNiHjQyn=DpmQ)ooEz5?f&mx>P;56aYm6o1vzpBTRn%dKYo<3T z+Obu4YFs$%W@H$-gl5P}I#=|FZ0fLK7)~A^ejye#3?y4$;_oOH3UC4$6hQ!@!3l!q zDVfu^;En9(kNMm|EMHe9@X7IHg`t*mW2FzfI6mOWFn}e_K{!tmKrQ4k^gcM9144cF zzQ=3{5TWbE5;zvrk>GXyLFbwv%$CUJ9E0xNFTg!EO(7|4#Hp|Ys4d-KTy(f^O{(95 zHDTBUD{7-_;`Qc98ToBFhgp%BNG$r+WJRyrA669?iihp)%_p_Bef<;T2#Y9sV$Rny z6FbXeY-opEx^*-G;6ytd4F`Ssi(^V1X!4ImAIH*%(oCd4Km>=qF7NVBWaBwT2JhON ziDMHZ!n8we-tF}VL#?Bo+w=|_$PPa{gEM5x;a)Omvkw;4mmF-b+v{i_h!K6@im!7U z>*yTW?n|v3f__X*6Eb9TOvDR`NbK>WoTp3I*cBlNh5bKvwBHc2f|vtA1m%2L-dd^n zh2kq&=J3d!{eq8T&ZM--DhnZ)GnFB_>tD8$Fin>W7zxp?3Vs32mPRp!9?QZAlcWl^ z>FiOq*C;ppXNulNEX4_#WTq0NT8?1KO-UN6Q5H@PuM?55PZL&@X6J?tn}p(sy)5Iw zpLR6&J0ZPG$}jIzrM2??=F02zGB(b`UPd~l8|EjXQ9qh%r`qNB2`K4D$6s%RPbZYc z{2nrWd4Y0IkrTjkZ13zIDp=yt@#O8T!^q*)3Ki(e9 z6Grt%h)rbpZ+>XK{y;m_bjXJYb-``GM$iC!lOaM&oP}p)%21d@OiRRVUPkOoaEXwW z6f7;sIHjCNzTf{PFWx02@5K}}oo-L-yy_a>SKkR2@=Y?7M91%mtPSg{I zE}SfJ82GzRPgF&q3o=&-hT&Egd;#voqR&Ihi>86V#0~>=4lMVLD*}B#BLy=ggqY`P zBgd={Py#AFm5a^C`upRj45=S^f( zoa)tP>d43w>luH$9k<}P>;JE^cW_79YM7BL;1hsYcx1vl?)@;BrWnWvxLvLZ<6Xci z0Y*N52)2;tY=EG9bF=3<$XzbECif6CM{v%H+6Wj3nhuzuIsd_RD1$b0yz}vvnRc6* zMj1idl0-g3`}QNX>mOQ20jVBa`xkK(o?l1dH*!{Pa>1anD!v2#}^2I^^uiw!F z6TXd7hcY3kWe@w@?#q5A@l^4k1XED1kC%2NG|dcJc6s5Wjp|sWE`;ptCL5DQq2h9( zG^3ybl!~Gyy>1PgT2A1=kbExoLXAZ;xjufSyn)D3N65JIZ%}S+J2mPbb#o($e_yXI zf~Brw%f%q2>(CXnzRD%MnPE1^$TarfWA>5%p`=j5UF0a#ynd=_*Vxz!SSo^M@~j}L zXG6T^)YhhXBTuYuL1Ps?EA;1})9>_z(>>?F@D;0|Q7<=;*e95pTF3>GuZW~4B*}c% zK+QiA^wXHC3h*wD0Zt$xeTq&rnm5T=``|BZnqUsG2r__V1E`1s4z&SG=m4FXCN_k> zQ9^M52MAYaA6y~^=G2w}$eafGf9V~vZEPUmz&T+D~XKcR^Ypf8~fb;jY$ zT|kEb?tSloqrHE3KzDE20srZMy`NXj=6i7}$rWLf7g^%Xmyv2+PERHg`?+yi0e>IU zHL02boB~NfHm)&Y!}j1E#3r1XCud5+?n9W3b!kXZ0{-Uo!uv;w z^?jiNRFdP?eD}kClNn;gCg;xSgB_KRgWTXPzp!fEP}V4*;%HXCy?Q`vvC9y}Pf_GrAGV3qyOv1JW!DwfZxQ=b*lCCI-WKm+6k_xi7{4ihcH%83 z|8e2{@IUaDUtSGC>0HoaBt-3S>XpN9bh$am3m9JtNpzeIIN9BFv=0v!b^}1YHr`D6 zwE7qt&E#O7R)h1OEMTah3%3 zD}n~HrR@6+ZITVViHP;}C_B4+x~`6)zC>H^N$MY$qdySCqFBhh7=UoIeo-^G;@Jr$ zD647dlH5YNl-=jKa;VB@Axf_Hxyva(G~r3$f6Op}_T~f8sn`o~)^*T$7}_@mFd_s{ zVxoLX2<>c3d@hJ5WtZ*Jk5bxayojqyzG?aXf|!+nMtQHDkc8xGr$p+SienP$Db2l< zS>ZpTy;QcBu6C<5Kc2x}ZFFJVyRDehR?KND=Cl=aQv36r0VU?7I(@5>c24c}JdqjQ zr*7BNpSCG1&xNsla%oY$^#hIQ7Fn$lk-yhhZ64aFtA66U3#uFgsk?n+?tljPE#WdK zts~;jg|bwavHwdkcQUqsnJrUyfhnzb`I3i-BSeiFeOJo1S)*U7b z0Eqzz4FK|ICcZ-8Zbhvg92&5vg5P>Eafz8$!-t8>$mY>f?ql4TO?aRh10A)bT z;`nH>ks@}_0NxD$XJcL@5VFv6wp8&ak1cR;klP`oP$KBktZ{eE@ZVs)?lOZsTS;JwV4Y6eu%|f;eBanH8 z4kqBk%3>QpZSWVw>_l*(xVy@^CL%))FBZu5FStU`HaD%|`nuS#mz#g??~Q8kpbw)l z2J|5|!+l`{i&!t{+TUwNwx-^u6TIQuLbBlMj!^)Eog^i0nC~3h4tv|-Uz2MB*Nm

&d{-zhR$Y!@?&ez49{oK5CpBC$88ASo5NACZkV`KjqD2-h3yPHT0r@Wm8WmqQH)^0c=kC9A{Zy#+>iTHZx&exGlyN-6}$)Id6TBb{%U7+81nSXXP4VV%F-v!H&948Ir7 zx^IQ^4T8_S)e<_({isCYlU_NKb4U4nkehAolXCqmoj9mwvUK85x0$7rG6E$rP@JXX zd5Qv7Z}W88Je|6%JWW$)hZZ<;8x*l7x%4Hq)x{&ug+k!nM$VXbkgm1 zL-#12o(QWme3#;IGCCM{JAMkQ`2AQ^kg!T5efdimRRid&2g9mVo%oq~`ye-z$>GXj zU5mRVgbG(lxL2T`k~vl2c-c+ih3ewY_6mINk0QSa*eArKKUrc!=Kz8QzVmNV2eS=0 zJ$Xfr;2ph$A|Qs0D`-nxTpmcGX(y#}Kwj#0X11?5?F&+;+lm5hMS;|c0vt*#w>49#;B8KQgs><_|`4&X!E+<4Pm^74Za zwkjshvm|j>DGwH&$Flhpu3X%D9*$f#m1!@VvJ)ekvJ>EQQc7aN3(nB>*h(o=v(4sh zvw7QW-Zq=J&E_qg&09&K*Q>>nZa0?2!M9Dfk~Uk))r3ly3@O1UPm)ELnyFck5BUOz z9NOZfiIcQ_!K=QAWjx7EqjD}?XG$Gsq6CqBS`a$Yy1|60I1VMqg42Q++)6G)ANOqE zq5BJ9GN-{^M$KGY6dOY7IYP3HiU-Vdb{+Qm-GXC*23Yrnam_(DH@(S`HjT74YH&^n zO!hS$)RuM2y%gTLaK{JFzYq%=1~T4n7&;{edJs1o!5Q}7_;z>)SVPylFN;pl6+w>E z0oDk3L6C)^;$f_ti58E|L-2-(Q!DQILkxTLtrc_?t;z;^U3}cva@D&y;YB$OV@AvM+ zdjvxqwUOctMvAxTCugIBUT%t}BRAZ&ObP*^)X9D%Y_CxH3;0dxeGl3E1N`=2o`e%s z6$1lgE6cJtugfF^jitIg{^L3I1+0yY+i1^N$Q_)3ucf^vE>n{T0oAwXxE<`vj3q#>=v?8y*TFr**<_9zTQ$ zBUk1-f1=Wy)WNi+y$y}HOpo5&m-)s$K1swEgjQXtM5v&oG93r5`Dbw;MC6MiYaK%i z>X>arMB$Nlt*iL;@iO|`y`I$&KR&|;@!v0wcuFYgm7`H9-I^5<1Q9V53 zSa8!q7}%c;EfKTwt;Nvo3(5-M>Zll+P@~} z5-?^GIDt`a}yob4sco$Gd0NTg}U?XGyn*2@~Is{is?0^+wOKSMVdgs_= z$aXQGK}S|E3vAeTzHQlDHQg*4+ur%bR=jIbZrB>~gW+H_Js3;|GbOJxG!el!dnHiKztJm;_cjIC_O5cPHFiuZ}4v zSw%#`tAWjf52g8?g92yxy*I@e;o%hf zN?)PQYPSh5XPeN9$+OX8=10v%lf*agpelMW6*;y-57=S**W{X<&c!j+{YS*~CFeH>u}%ye zqwXD#p_@3|Bd$4;Rs6+;jnH&NL2rqaIG@@B?g^LpehKel=VvCBH*&l&DES8iE=x(U z!cY`|XGma#40)?Z3Ao~OLYcr-oQNSKI1MX07K`>0N6yDB2{;uVIGxLs3(G{;1PDyU zd7&eY_57vk0K|8q;v~`QLZ`&R@(2+V1K(j|&Bxbqb2QcjQzEL_nWkJc-hqEHCxE(6 zS1T&Q^#yRhZ@-7oqBS04->S``q2&)6g9+H}1JR7bO@HFkx@u1{Ynj;ntmHn)s-F-$qxx;d!o5)# z>BTnUGD0*}For#-Ii_$b=qe^~Xarj+(4C5Cp%(U9sTeG>ZBS)a%PIlT>kl}Ki0H?| zuiZ|!)9Z9Q-J6bffuNxse(iS#{6VLC^H?^U!9}84Jw4fd)5UvM@ZnSf9Cfr6Cc%%m z)ClHnXH2|MfdhBJo@;tuUAgngAV0@4)gUS%Gx_%_UfZYuE#*z%&D4ZvJ^z6_lqWdJ z*eOqKft=4FlQx;IO{Qz3314WHuE{Vzfw}rkaKZO^ci;pv@+Q1LKS8>wpfd_ChAZr% zivGyGC$5p3adHsOxFje43l%}FB%?E3jpAmVm@n?Izm>amp3H=O+4kZnKb5g|OiNZY zOB&-#v8=-G*>NrD{O*R$RN-tf$q~2Boaj~$g5fwc6#n84=g9(J+EcMtaw_)o45N*SIU7w5@>7p4kRh{b zYFiXhNLiBfmqZMqT0T(}dP6cYf%21twf|Dy^VAzmrx13Bd;M{Lv^N~idV4cy4EJV6 z_h2@f&iZ|QxSbfmHpAq}ICYYrVRC^uS^tWl$!g`9gyEFPHt`7oBt&DA%RD)C53TxHvy*TXz42r`+8g#J)4j=j4)>;WH1EzQ-7!KabV*%Nta`WW#;|Dz z%QYWB`!67 z$L)`cHTer-rX65i;_jS19eG zY{?|qK`O5$vTHib$OMqG@sdGSN56^hRTNxYD2oq^6`W|Mtl?5Ssi5J!Q9Xf*OGUXr z#btfdoQnIQiJ<<7{FX|hqn$1Ywb2D~=-Sppav}8OhxQq*&X*ep>(JB=`v=3x!NIgU zo(_i{{z`hf`n}=7a55MVC*tAdJ+!pLUVkzfPP@IyWYYB?C)*#Ne{rDDN>b=)L7#h~IU^*Jre%eA4<|fe3tvKKOM0Bo-Ni=i?ZOeW)Q6dV6pg9wHD ziR09d;neFM3=W39Nq-nYt2gd-$G!f+VB9EXL2unu>;^IH2K|1o-yaPR`rRmc-9h(Y z+V72e-JueKd+8XC6Bvfl=uA}lkzWI$CUyfkOIic7SZ_trf7qW6r{jbE zbR0o0{P^jRi_8AWXfmA+hNEdex}c%QPlsGZ$-fP%uO<1nMD?m$fM=olxDwbYs*kIH z9isY>0&vewdd(_;#G~g{0ICQ*y#i3htfT_iDdZY3{^_K<5}53nudk_w>TI(=Ttd4P{zw_#^vQ|AHw?y%eIPKV>sxIGWpB?fpQ=K;^g z0IKH!&&2>CwO~L7b!?<*g`L`E#r4EaAs1H|JB8d&w`{xX&8g^`u({aV7D**d(_R&xuXaarB(nBt1pX ziA`M4KQA_M$^N|9B=-W(iA~ZG^qklvJwMNfO-MQiX%Y>)`(MR*@1#2#bjRM-uA=u& zUEu~4QzWs+-Cln>8uoj`=r%mE*J~a(MFHOL_D9q1WZ0j?CT?&r989L8!SJBzldl}~ z+SA@r4xXK%`M7-FDVk4+`(2{>RQX@W+n<7FhWqIm6@W5ETlV+YL9OhGZ{XIJvXNzC9j+|{ zGSo%`HK-igXrMM4Xg8EX8x8cF$o(AAKq<P6wnT((6mPhZQ|Gyh5GCe)koAoXp_)tfmRE&TAiP~>%)84=}TRp#T9+tRN#oR_P)suj*}y#;UhOg$nH|UR4#U zn43*}&Cq3x=M`uzY$VL>9aD^qf!mN+LNddD?xTb2CB3V ui@#j@YM|PG+W1^~uFmMqU1YhdHRDGE?eX#E@&5$?0RR7IhnXPR76SktJ{p|> literal 16049 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 Dz3`Mq delta 3865 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 bITSt=D$;s=xBmYC00960T(a21Z!rM?G`EKU diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 1cbe41160..4ea7def6a 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -79,6 +79,9 @@ * [EthGetBlockTransactionCountByHash](#EthGetBlockTransactionCountByHash) * [EthGetBlockTransactionCountByNumber](#EthGetBlockTransactionCountByNumber) * [EthGetCode](#EthGetCode) + * [EthGetFilterChanges](#EthGetFilterChanges) + * [EthGetFilterLogs](#EthGetFilterLogs) + * [EthGetLogs](#EthGetLogs) * [EthGetStorageAt](#EthGetStorageAt) * [EthGetTransactionByBlockHashAndIndex](#EthGetTransactionByBlockHashAndIndex) * [EthGetTransactionByBlockNumberAndIndex](#EthGetTransactionByBlockNumberAndIndex) @@ -86,8 +89,14 @@ * [EthGetTransactionCount](#EthGetTransactionCount) * [EthGetTransactionReceipt](#EthGetTransactionReceipt) * [EthMaxPriorityFeePerGas](#EthMaxPriorityFeePerGas) + * [EthNewBlockFilter](#EthNewBlockFilter) + * [EthNewFilter](#EthNewFilter) + * [EthNewPendingTransactionFilter](#EthNewPendingTransactionFilter) * [EthProtocolVersion](#EthProtocolVersion) * [EthSendRawTransaction](#EthSendRawTransaction) + * [EthSubscribe](#EthSubscribe) + * [EthUninstallFilter](#EthUninstallFilter) + * [EthUnsubscribe](#EthUnsubscribe) * [Gas](#Gas) * [GasEstimateFeeCap](#GasEstimateFeeCap) * [GasEstimateGasLimit](#GasEstimateGasLimit) @@ -2413,6 +2422,74 @@ Inputs: Response: `"0x07"` +### EthGetFilterChanges +Polling method for a filter, returns event logs which occurred since last poll. +(requires write perm since timestamp of last filter execution will be written) + + +Perms: write + +Inputs: +```json +[ + "c5564560217c43e4bc0484df655e9019" +] +``` + +Response: +```json +[ + "0x0707070707070707070707070707070707070707070707070707070707070707" +] +``` + +### EthGetFilterLogs +Returns event logs matching filter with given id. +(requires write perm since timestamp of last filter execution will be written) + + +Perms: write + +Inputs: +```json +[ + "c5564560217c43e4bc0484df655e9019" +] +``` + +Response: +```json +[ + "0x0707070707070707070707070707070707070707070707070707070707070707" +] +``` + +### EthGetLogs +Returns event logs matching given filter spec. + + +Perms: read + +Inputs: +```json +[ + { + "fromBlock": "2301220", + "address": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ], + "topics": null + } +] +``` + +Response: +```json +[ + "0x0707070707070707070707070707070707070707070707070707070707070707" +] +``` + ### EthGetStorageAt @@ -2597,6 +2674,47 @@ Inputs: `null` Response: `"0x0"` +### EthNewBlockFilter +Installs a persistent filter to notify when a new block arrives. + + +Perms: write + +Inputs: `null` + +Response: `"c5564560217c43e4bc0484df655e9019"` + +### EthNewFilter +Installs a persistent filter based on given filter spec. + + +Perms: write + +Inputs: +```json +[ + { + "fromBlock": "2301220", + "address": [ + "0x5cbeecf99d3fdb3f25e309cc264f240bb0664031" + ], + "topics": null + } +] +``` + +Response: `"c5564560217c43e4bc0484df655e9019"` + +### EthNewPendingTransactionFilter +Installs a persistent filter to notify when new messages arrive in the message pool. + + +Perms: write + +Inputs: `null` + +Response: `"c5564560217c43e4bc0484df655e9019"` + ### EthProtocolVersion @@ -2620,6 +2738,72 @@ Inputs: Response: `"0x0707070707070707070707070707070707070707070707070707070707070707"` +### EthSubscribe +Subscribe to different event types using websockets +eventTypes is one or more of: + - newHeads: notify when new blocks arrive. + - pendingTransactions: notify when new messages arrive in the message pool. + - logs: notify new event logs that match a criteria +params contains additional parameters used with the log event type +The client will receive a stream of EthSubscriptionResponse values until EthUnsubscribe is called. + + +Perms: write + +Inputs: +```json +[ + [ + "string value" + ], + { + "topics": [ + [ + "0x0707070707070707070707070707070707070707070707070707070707070707" + ] + ] + } +] +``` + +Response: +```json +{ + "subscription": "b62df77831484129adf6682332ad0725", + "result": {} +} +``` + +### EthUninstallFilter +Uninstalls a filter with given id. + + +Perms: write + +Inputs: +```json +[ + "c5564560217c43e4bc0484df655e9019" +] +``` + +Response: `true` + +### EthUnsubscribe +Unsubscribe from a websocket subscription + + +Perms: write + +Inputs: +```json +[ + "b62df77831484129adf6682332ad0725" +] +``` + +Response: `true` + ## Gas diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 902cb1a05..f8f4c5fdb 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -29,6 +29,47 @@ var Doc = map[string][]DocField{ Comment: ``, }, }, + "ActorEventConfig": []DocField{ + { + Name: "EnableRealTimeFilterAPI", + Type: "bool", + + Comment: `EnableRealTimeFilterAPI enables APIs that can create and query filters for actor events as they are emitted.`, + }, + { + Name: "EnableHistoricFilterAPI", + Type: "bool", + + Comment: `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.`, + }, + { + Name: "FilterTTL", + Type: "Duration", + + Comment: `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.`, + }, + { + Name: "MaxFilters", + Type: "int", + + Comment: `MaxFilters specifies the maximum number of filters that may exist at any one time.`, + }, + { + Name: "MaxFilterResults", + Type: "int", + + Comment: `MaxFilterResults specifies the maximum number of results that can be accumulated by an actor event filter.`, + }, + { + Name: "MaxFilterHeightRange", + Type: "uint64", + + Comment: `MaxFilterHeightRange specifies the maximum range of heights that can be used in a filter (to avoid querying +the entire chain)`, + }, + }, "Backup": []DocField{ { Name: "DisableMetadataLog", @@ -372,6 +413,12 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/# Name: "Chainstore", Type: "Chainstore", + Comment: ``, + }, + { + Name: "ActorEvent", + Type: "ActorEventConfig", + Comment: ``, }, }, From 91952f84bdc6909e945f24675079ab84eaf9180c Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Mon, 14 Nov 2022 10:27:06 +0000 Subject: [PATCH 3/4] Fix eth types comments --- api/eth_types.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/api/eth_types.go b/api/eth_types.go index d8dd6e64d..559205ef7 100644 --- a/api/eth_types.go +++ b/api/eth_types.go @@ -470,7 +470,7 @@ type EthFilterSpec struct { // Optional, default: empty list Topics EthTopicSpec `json:"topics"` - // Restricts event logs returned to those in receipts contained in the tipset this block is part of. + // Restricts event logs returned to those emitted from messages contained in this tipset. // If BlockHash is present in in the filter criteria, then neither FromBlock nor ToBlock are allowed. // Added in EIP-234 BlockHash *EthHash `json:"blockHash,omitempty"` @@ -506,7 +506,6 @@ func (e *EthAddressList) UnmarshalJSON(b []byte) error { // An event log with topics [A, B] will be matched by the following topic specs: // [] "all" // [[A]] "A in first position (and anything after)" -// [[A]] "A in first position (and anything after)" // [nil, [B] ] "anything in first position AND B in second position (and anything after)" // [[A], [B]] "A in first position AND B in second position (and anything after)" // [[A, B], [A, B]] "(A OR B) in first position AND (A OR B) in second position (and anything after)" @@ -515,6 +514,8 @@ func (e *EthAddressList) UnmarshalJSON(b []byte) error { // { "A", [ "B", "C" ] } must be decoded as [ [ A ], [ B, C ] ] type EthTopicSpec []EthHashList +// EthHashList represents a list of EthHashes. +// The JSON decoding treats string values as equivalent to arrays with one value. type EthHashList []EthHash func (e *EthHashList) UnmarshalJSON(b []byte) error { @@ -567,6 +568,7 @@ func (h EthFilterResult) MarshalJSON() ([]byte, error) { return []byte{'[', ']'}, nil } +// EthLog represents the results of an event filter execution. type EthLog struct { // Address is the address of the actor that produced the event log. Address EthAddress `json:"address"` @@ -590,16 +592,13 @@ type EthLog struct { // The index corresponds to the sequence of messages produced by ChainGetParentMessages TransactionIndex EthUint64 `json:"transactionIndex"` - // TransactionHash is the cid of the transaction that produced the event log. + // TransactionHash is the cid of the message that produced the event log. TransactionHash EthHash `json:"transactionHash"` - // BlockHash is the hash of a block in the tipset containing the message receipt of the message execution. - // This may be passed to ChainGetParentReceipts to obtain a list of receipts. The receipt - // containing the events will be at TransactionIndex in the receipt list. + // BlockHash is the hash of the tipset containing the message that produced the log. BlockHash EthHash `json:"blockHash"` - // BlockNumber is the epoch at which the message was executed. This is the epoch containing - // the message receipt. + // BlockNumber is the epoch of the tipset containing the message. BlockNumber EthUint64 `json:"blockNumber"` } From efdbea5b53c37ea5115da385321496b099d93967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Mon, 14 Nov 2022 11:55:04 +0000 Subject: [PATCH 4/4] fix Receipt#EventsRoot is now *cid.Cid. --- chain/events/filter/event.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chain/events/filter/event.go b/chain/events/filter/event.go index c81aed7fe..eca6a2bda 100644 --- a/chain/events/filter/event.go +++ b/chain/events/filter/event.go @@ -375,7 +375,11 @@ func (m *EventFilterManager) loadExecutedMessages(ctx context.Context, msgTs, rc } ems[i].rct = &rct - evtArr, err := blockadt.AsArray(st, rct.EventsRoot) + if rct.EventsRoot == nil { + continue + } + + evtArr, err := blockadt.AsArray(st, *rct.EventsRoot) if err != nil { return nil, xerrors.Errorf("load events amt: %w", err) }