github.com/ethereum-optimism/optimism/l2geth@v0.0.0-20230612200230-50b04ade19e3/eth/filters/api.go (about)

     1  // Copyright 2015 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package filters
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"math/big"
    25  	"sync"
    26  	"time"
    27  
    28  	ethereum "github.com/ethereum-optimism/optimism/l2geth"
    29  	"github.com/ethereum-optimism/optimism/l2geth/common"
    30  	"github.com/ethereum-optimism/optimism/l2geth/common/hexutil"
    31  	"github.com/ethereum-optimism/optimism/l2geth/core/types"
    32  	"github.com/ethereum-optimism/optimism/l2geth/ethdb"
    33  	"github.com/ethereum-optimism/optimism/l2geth/event"
    34  	"github.com/ethereum-optimism/optimism/l2geth/rpc"
    35  )
    36  
    37  var (
    38  	deadline = 5 * time.Minute // consider a filter inactive if it has not been polled for within deadline
    39  )
    40  
    41  // filter is a helper struct that holds meta information over the filter type
    42  // and associated subscription in the event system.
    43  type filter struct {
    44  	typ      Type
    45  	deadline *time.Timer // filter is inactiv when deadline triggers
    46  	hashes   []common.Hash
    47  	crit     FilterCriteria
    48  	logs     []*types.Log
    49  	s        *Subscription // associated subscription in event system
    50  }
    51  
    52  // PublicFilterAPI offers support to create and manage filters. This will allow external clients to retrieve various
    53  // information related to the Ethereum protocol such als blocks, transactions and logs.
    54  type PublicFilterAPI struct {
    55  	backend   Backend
    56  	mux       *event.TypeMux
    57  	quit      chan struct{}
    58  	chainDb   ethdb.Database
    59  	events    *EventSystem
    60  	filtersMu sync.Mutex
    61  	filters   map[rpc.ID]*filter
    62  }
    63  
    64  // NewPublicFilterAPI returns a new PublicFilterAPI instance.
    65  func NewPublicFilterAPI(backend Backend, lightMode bool) *PublicFilterAPI {
    66  	api := &PublicFilterAPI{
    67  		backend: backend,
    68  		chainDb: backend.ChainDb(),
    69  		events:  NewEventSystem(backend, lightMode),
    70  		filters: make(map[rpc.ID]*filter),
    71  	}
    72  	go api.timeoutLoop()
    73  
    74  	return api
    75  }
    76  
    77  // timeoutLoop runs every 5 minutes and deletes filters that have not been recently used.
    78  // Tt is started when the api is created.
    79  func (api *PublicFilterAPI) timeoutLoop() {
    80  	ticker := time.NewTicker(5 * time.Minute)
    81  	for {
    82  		<-ticker.C
    83  		api.filtersMu.Lock()
    84  		for id, f := range api.filters {
    85  			select {
    86  			case <-f.deadline.C:
    87  				f.s.Unsubscribe()
    88  				delete(api.filters, id)
    89  			default:
    90  				continue
    91  			}
    92  		}
    93  		api.filtersMu.Unlock()
    94  	}
    95  }
    96  
    97  // NewPendingTransactionFilter creates a filter that fetches pending transaction hashes
    98  // as transactions enter the pending state.
    99  //
   100  // It is part of the filter package because this filter can be used through the
   101  // `eth_getFilterChanges` polling method that is also used for log filters.
   102  //
   103  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newpendingtransactionfilter
   104  func (api *PublicFilterAPI) NewPendingTransactionFilter() rpc.ID {
   105  	var (
   106  		pendingTxs   = make(chan []common.Hash)
   107  		pendingTxSub = api.events.SubscribePendingTxs(pendingTxs)
   108  	)
   109  
   110  	api.filtersMu.Lock()
   111  	api.filters[pendingTxSub.ID] = &filter{typ: PendingTransactionsSubscription, deadline: time.NewTimer(deadline), hashes: make([]common.Hash, 0), s: pendingTxSub}
   112  	api.filtersMu.Unlock()
   113  
   114  	go func() {
   115  		for {
   116  			select {
   117  			case ph := <-pendingTxs:
   118  				api.filtersMu.Lock()
   119  				if f, found := api.filters[pendingTxSub.ID]; found {
   120  					f.hashes = append(f.hashes, ph...)
   121  				}
   122  				api.filtersMu.Unlock()
   123  			case <-pendingTxSub.Err():
   124  				api.filtersMu.Lock()
   125  				delete(api.filters, pendingTxSub.ID)
   126  				api.filtersMu.Unlock()
   127  				return
   128  			}
   129  		}
   130  	}()
   131  
   132  	return pendingTxSub.ID
   133  }
   134  
   135  // NewPendingTransactions creates a subscription that is triggered each time a transaction
   136  // enters the transaction pool and was signed from one of the transactions this nodes manages.
   137  func (api *PublicFilterAPI) NewPendingTransactions(ctx context.Context) (*rpc.Subscription, error) {
   138  	notifier, supported := rpc.NotifierFromContext(ctx)
   139  	if !supported {
   140  		return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
   141  	}
   142  
   143  	rpcSub := notifier.CreateSubscription()
   144  
   145  	go func() {
   146  		txHashes := make(chan []common.Hash, 128)
   147  		pendingTxSub := api.events.SubscribePendingTxs(txHashes)
   148  
   149  		for {
   150  			select {
   151  			case hashes := <-txHashes:
   152  				// To keep the original behaviour, send a single tx hash in one notification.
   153  				// TODO(rjl493456442) Send a batch of tx hashes in one notification
   154  				for _, h := range hashes {
   155  					notifier.Notify(rpcSub.ID, h)
   156  				}
   157  			case <-rpcSub.Err():
   158  				pendingTxSub.Unsubscribe()
   159  				return
   160  			case <-notifier.Closed():
   161  				pendingTxSub.Unsubscribe()
   162  				return
   163  			}
   164  		}
   165  	}()
   166  
   167  	return rpcSub, nil
   168  }
   169  
   170  // NewBlockFilter creates a filter that fetches blocks that are imported into the chain.
   171  // It is part of the filter package since polling goes with eth_getFilterChanges.
   172  //
   173  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter
   174  func (api *PublicFilterAPI) NewBlockFilter() rpc.ID {
   175  	var (
   176  		headers   = make(chan *types.Header)
   177  		headerSub = api.events.SubscribeNewHeads(headers)
   178  	)
   179  
   180  	api.filtersMu.Lock()
   181  	api.filters[headerSub.ID] = &filter{typ: BlocksSubscription, deadline: time.NewTimer(deadline), hashes: make([]common.Hash, 0), s: headerSub}
   182  	api.filtersMu.Unlock()
   183  
   184  	go func() {
   185  		for {
   186  			select {
   187  			case h := <-headers:
   188  				api.filtersMu.Lock()
   189  				if f, found := api.filters[headerSub.ID]; found {
   190  					f.hashes = append(f.hashes, h.Hash())
   191  				}
   192  				api.filtersMu.Unlock()
   193  			case <-headerSub.Err():
   194  				api.filtersMu.Lock()
   195  				delete(api.filters, headerSub.ID)
   196  				api.filtersMu.Unlock()
   197  				return
   198  			}
   199  		}
   200  	}()
   201  
   202  	return headerSub.ID
   203  }
   204  
   205  // NewHeads send a notification each time a new (header) block is appended to the chain.
   206  func (api *PublicFilterAPI) NewHeads(ctx context.Context) (*rpc.Subscription, error) {
   207  	notifier, supported := rpc.NotifierFromContext(ctx)
   208  	if !supported {
   209  		return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
   210  	}
   211  
   212  	rpcSub := notifier.CreateSubscription()
   213  
   214  	go func() {
   215  		headers := make(chan *types.Header)
   216  		headersSub := api.events.SubscribeNewHeads(headers)
   217  
   218  		for {
   219  			select {
   220  			case h := <-headers:
   221  				notifier.Notify(rpcSub.ID, h)
   222  			case <-rpcSub.Err():
   223  				headersSub.Unsubscribe()
   224  				return
   225  			case <-notifier.Closed():
   226  				headersSub.Unsubscribe()
   227  				return
   228  			}
   229  		}
   230  	}()
   231  
   232  	return rpcSub, nil
   233  }
   234  
   235  // Logs creates a subscription that fires for all new log that match the given filter criteria.
   236  func (api *PublicFilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*rpc.Subscription, error) {
   237  	notifier, supported := rpc.NotifierFromContext(ctx)
   238  	if !supported {
   239  		return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
   240  	}
   241  
   242  	var (
   243  		rpcSub      = notifier.CreateSubscription()
   244  		matchedLogs = make(chan []*types.Log)
   245  	)
   246  
   247  	logsSub, err := api.events.SubscribeLogs(ethereum.FilterQuery(crit), matchedLogs)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	go func() {
   253  
   254  		for {
   255  			select {
   256  			case logs := <-matchedLogs:
   257  				for _, log := range logs {
   258  					notifier.Notify(rpcSub.ID, &log)
   259  				}
   260  			case <-rpcSub.Err(): // client send an unsubscribe request
   261  				logsSub.Unsubscribe()
   262  				return
   263  			case <-notifier.Closed(): // connection dropped
   264  				logsSub.Unsubscribe()
   265  				return
   266  			}
   267  		}
   268  	}()
   269  
   270  	return rpcSub, nil
   271  }
   272  
   273  // FilterCriteria represents a request to create a new filter.
   274  // Same as ethereum.FilterQuery but with UnmarshalJSON() method.
   275  type FilterCriteria ethereum.FilterQuery
   276  
   277  // NewFilter creates a new filter and returns the filter id. It can be
   278  // used to retrieve logs when the state changes. This method cannot be
   279  // used to fetch logs that are already stored in the state.
   280  //
   281  // Default criteria for the from and to block are "latest".
   282  // Using "latest" as block number will return logs for mined blocks.
   283  // Using "pending" as block number returns logs for not yet mined (pending) blocks.
   284  // In case logs are removed (chain reorg) previously returned logs are returned
   285  // again but with the removed property set to true.
   286  //
   287  // In case "fromBlock" > "toBlock" an error is returned.
   288  //
   289  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter
   290  func (api *PublicFilterAPI) NewFilter(crit FilterCriteria) (rpc.ID, error) {
   291  	logs := make(chan []*types.Log)
   292  	logsSub, err := api.events.SubscribeLogs(ethereum.FilterQuery(crit), logs)
   293  	if err != nil {
   294  		return rpc.ID(""), err
   295  	}
   296  
   297  	api.filtersMu.Lock()
   298  	api.filters[logsSub.ID] = &filter{typ: LogsSubscription, crit: crit, deadline: time.NewTimer(deadline), logs: make([]*types.Log, 0), s: logsSub}
   299  	api.filtersMu.Unlock()
   300  
   301  	go func() {
   302  		for {
   303  			select {
   304  			case l := <-logs:
   305  				api.filtersMu.Lock()
   306  				if f, found := api.filters[logsSub.ID]; found {
   307  					f.logs = append(f.logs, l...)
   308  				}
   309  				api.filtersMu.Unlock()
   310  			case <-logsSub.Err():
   311  				api.filtersMu.Lock()
   312  				delete(api.filters, logsSub.ID)
   313  				api.filtersMu.Unlock()
   314  				return
   315  			}
   316  		}
   317  	}()
   318  
   319  	return logsSub.ID, nil
   320  }
   321  
   322  // GetLogs returns logs matching the given argument that are stored within the state.
   323  //
   324  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs
   325  func (api *PublicFilterAPI) GetLogs(ctx context.Context, crit FilterCriteria) ([]*types.Log, error) {
   326  	var filter *Filter
   327  	if crit.BlockHash != nil {
   328  		// Block filter requested, construct a single-shot filter
   329  		filter = NewBlockFilter(api.backend, *crit.BlockHash, crit.Addresses, crit.Topics)
   330  	} else {
   331  		// Convert the RPC block numbers into internal representations
   332  		begin := rpc.LatestBlockNumber.Int64()
   333  		if crit.FromBlock != nil {
   334  			begin = crit.FromBlock.Int64()
   335  		}
   336  		end := rpc.LatestBlockNumber.Int64()
   337  		if crit.ToBlock != nil {
   338  			end = crit.ToBlock.Int64()
   339  		}
   340  		// Construct the range filter
   341  		filter = NewRangeFilter(api.backend, begin, end, crit.Addresses, crit.Topics)
   342  	}
   343  	// Run the filter and return all the logs
   344  	logs, err := filter.Logs(ctx)
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  	return returnLogs(logs), err
   349  }
   350  
   351  // UninstallFilter removes the filter with the given filter id.
   352  //
   353  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter
   354  func (api *PublicFilterAPI) UninstallFilter(id rpc.ID) bool {
   355  	api.filtersMu.Lock()
   356  	f, found := api.filters[id]
   357  	if found {
   358  		delete(api.filters, id)
   359  	}
   360  	api.filtersMu.Unlock()
   361  	if found {
   362  		f.s.Unsubscribe()
   363  	}
   364  
   365  	return found
   366  }
   367  
   368  // GetFilterLogs returns the logs for the filter with the given id.
   369  // If the filter could not be found an empty array of logs is returned.
   370  //
   371  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs
   372  func (api *PublicFilterAPI) GetFilterLogs(ctx context.Context, id rpc.ID) ([]*types.Log, error) {
   373  	api.filtersMu.Lock()
   374  	f, found := api.filters[id]
   375  	api.filtersMu.Unlock()
   376  
   377  	if !found || f.typ != LogsSubscription {
   378  		return nil, fmt.Errorf("filter not found")
   379  	}
   380  
   381  	var filter *Filter
   382  	if f.crit.BlockHash != nil {
   383  		// Block filter requested, construct a single-shot filter
   384  		filter = NewBlockFilter(api.backend, *f.crit.BlockHash, f.crit.Addresses, f.crit.Topics)
   385  	} else {
   386  		// Convert the RPC block numbers into internal representations
   387  		begin := rpc.LatestBlockNumber.Int64()
   388  		if f.crit.FromBlock != nil {
   389  			begin = f.crit.FromBlock.Int64()
   390  		}
   391  		end := rpc.LatestBlockNumber.Int64()
   392  		if f.crit.ToBlock != nil {
   393  			end = f.crit.ToBlock.Int64()
   394  		}
   395  		// Construct the range filter
   396  		filter = NewRangeFilter(api.backend, begin, end, f.crit.Addresses, f.crit.Topics)
   397  	}
   398  	// Run the filter and return all the logs
   399  	logs, err := filter.Logs(ctx)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  	return returnLogs(logs), nil
   404  }
   405  
   406  // GetFilterChanges returns the logs for the filter with the given id since
   407  // last time it was called. This can be used for polling.
   408  //
   409  // For pending transaction and block filters the result is []common.Hash.
   410  // (pending)Log filters return []Log.
   411  //
   412  // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges
   413  func (api *PublicFilterAPI) GetFilterChanges(id rpc.ID) (interface{}, error) {
   414  	api.filtersMu.Lock()
   415  	defer api.filtersMu.Unlock()
   416  
   417  	if f, found := api.filters[id]; found {
   418  		if !f.deadline.Stop() {
   419  			// timer expired but filter is not yet removed in timeout loop
   420  			// receive timer value and reset timer
   421  			<-f.deadline.C
   422  		}
   423  		f.deadline.Reset(deadline)
   424  
   425  		switch f.typ {
   426  		case PendingTransactionsSubscription, BlocksSubscription:
   427  			hashes := f.hashes
   428  			f.hashes = nil
   429  			return returnHashes(hashes), nil
   430  		case LogsSubscription, MinedAndPendingLogsSubscription:
   431  			logs := f.logs
   432  			f.logs = nil
   433  			return returnLogs(logs), nil
   434  		}
   435  	}
   436  
   437  	return []interface{}{}, fmt.Errorf("filter not found")
   438  }
   439  
   440  // returnHashes is a helper that will return an empty hash array case the given hash array is nil,
   441  // otherwise the given hashes array is returned.
   442  func returnHashes(hashes []common.Hash) []common.Hash {
   443  	if hashes == nil {
   444  		return []common.Hash{}
   445  	}
   446  	return hashes
   447  }
   448  
   449  // returnLogs is a helper that will return an empty log array in case the given logs array is nil,
   450  // otherwise the given logs array is returned.
   451  func returnLogs(logs []*types.Log) []*types.Log {
   452  	if logs == nil {
   453  		return []*types.Log{}
   454  	}
   455  	return logs
   456  }
   457  
   458  // UnmarshalJSON sets *args fields with given data.
   459  func (args *FilterCriteria) UnmarshalJSON(data []byte) error {
   460  	type input struct {
   461  		BlockHash *common.Hash     `json:"blockHash"`
   462  		FromBlock *rpc.BlockNumber `json:"fromBlock"`
   463  		ToBlock   *rpc.BlockNumber `json:"toBlock"`
   464  		Addresses interface{}      `json:"address"`
   465  		Topics    []interface{}    `json:"topics"`
   466  	}
   467  
   468  	var raw input
   469  	if err := json.Unmarshal(data, &raw); err != nil {
   470  		return err
   471  	}
   472  
   473  	if raw.BlockHash != nil {
   474  		if raw.FromBlock != nil || raw.ToBlock != nil {
   475  			// BlockHash is mutually exclusive with FromBlock/ToBlock criteria
   476  			return fmt.Errorf("cannot specify both BlockHash and FromBlock/ToBlock, choose one or the other")
   477  		}
   478  		args.BlockHash = raw.BlockHash
   479  	} else {
   480  		if raw.FromBlock != nil {
   481  			args.FromBlock = big.NewInt(raw.FromBlock.Int64())
   482  		}
   483  
   484  		if raw.ToBlock != nil {
   485  			args.ToBlock = big.NewInt(raw.ToBlock.Int64())
   486  		}
   487  	}
   488  
   489  	args.Addresses = []common.Address{}
   490  
   491  	if raw.Addresses != nil {
   492  		// raw.Address can contain a single address or an array of addresses
   493  		switch rawAddr := raw.Addresses.(type) {
   494  		case []interface{}:
   495  			for i, addr := range rawAddr {
   496  				if strAddr, ok := addr.(string); ok {
   497  					addr, err := decodeAddress(strAddr)
   498  					if err != nil {
   499  						return fmt.Errorf("invalid address at index %d: %v", i, err)
   500  					}
   501  					args.Addresses = append(args.Addresses, addr)
   502  				} else {
   503  					return fmt.Errorf("non-string address at index %d", i)
   504  				}
   505  			}
   506  		case string:
   507  			addr, err := decodeAddress(rawAddr)
   508  			if err != nil {
   509  				return fmt.Errorf("invalid address: %v", err)
   510  			}
   511  			args.Addresses = []common.Address{addr}
   512  		default:
   513  			return errors.New("invalid addresses in query")
   514  		}
   515  	}
   516  
   517  	// topics is an array consisting of strings and/or arrays of strings.
   518  	// JSON null values are converted to common.Hash{} and ignored by the filter manager.
   519  	if len(raw.Topics) > 0 {
   520  		args.Topics = make([][]common.Hash, len(raw.Topics))
   521  		for i, t := range raw.Topics {
   522  			switch topic := t.(type) {
   523  			case nil:
   524  				// ignore topic when matching logs
   525  
   526  			case string:
   527  				// match specific topic
   528  				top, err := decodeTopic(topic)
   529  				if err != nil {
   530  					return err
   531  				}
   532  				args.Topics[i] = []common.Hash{top}
   533  
   534  			case []interface{}:
   535  				// or case e.g. [null, "topic0", "topic1"]
   536  				for _, rawTopic := range topic {
   537  					if rawTopic == nil {
   538  						// null component, match all
   539  						args.Topics[i] = nil
   540  						break
   541  					}
   542  					if topic, ok := rawTopic.(string); ok {
   543  						parsed, err := decodeTopic(topic)
   544  						if err != nil {
   545  							return err
   546  						}
   547  						args.Topics[i] = append(args.Topics[i], parsed)
   548  					} else {
   549  						return fmt.Errorf("invalid topic(s)")
   550  					}
   551  				}
   552  			default:
   553  				return fmt.Errorf("invalid topic(s)")
   554  			}
   555  		}
   556  	}
   557  
   558  	return nil
   559  }
   560  
   561  func decodeAddress(s string) (common.Address, error) {
   562  	b, err := hexutil.Decode(s)
   563  	if err == nil && len(b) != common.AddressLength {
   564  		err = fmt.Errorf("hex has invalid length %d after decoding; expected %d for address", len(b), common.AddressLength)
   565  	}
   566  	return common.BytesToAddress(b), err
   567  }
   568  
   569  func decodeTopic(s string) (common.Hash, error) {
   570  	b, err := hexutil.Decode(s)
   571  	if err == nil && len(b) != common.HashLength {
   572  		err = fmt.Errorf("hex has invalid length %d after decoding; expected %d for topic", len(b), common.HashLength)
   573  	}
   574  	return common.BytesToHash(b), err
   575  }