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