-
-
Notifications
You must be signed in to change notification settings - Fork 746
Expand file tree
/
Copy pathethrpc.go
More file actions
2638 lines (2443 loc) · 97.5 KB
/
Copy pathethrpc.go
File metadata and controls
2638 lines (2443 loc) · 97.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package eth
import (
"context"
"encoding/hex"
"encoding/json"
stdErrors "errors"
"fmt"
"io"
"math/big"
"net/http"
"net/url"
"runtime/debug"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
"github.com/golang/glog"
"github.com/juju/errors"
"github.com/trezor/blockbook/bchain"
"github.com/trezor/blockbook/common"
"golang.org/x/crypto/sha3"
"golang.org/x/sync/singleflight"
)
// Network type specifies the type of ethereum network
type Network uint32
const (
// MainNet is production network
MainNet Network = 1
// TestNetSepolia is Sepolia test network
TestNetSepolia Network = 11155111
// TestNetHoodi is Hoodi test network
TestNetHoodi Network = 560048
)
const (
defaultErc20BatchSize = 100
// defaultRPCTimeoutSeconds is used when rpc_timeout is unset or non-positive.
// A zero b.Timeout makes context.WithTimeout expire immediately (breaking every
// call), so a finite floor is enforced rather than trusting the config. Kept
// above the 10s trace_timeout default so the fallback still lets a block's
// internal-data trace finish.
defaultRPCTimeoutSeconds = 15
// Alternative/private relays expire pending txs quickly, so local pending state
// must not inherit the legacy hour-scale public mempool timeout.
defaultMempoolTxTimeoutWithAlternativeProvider = 10 * time.Minute
defaultAlternativeMempoolTxTimeout = 5 * time.Minute
)
// Ethereum address constants
const (
// EthereumZeroAddress is the zero address (0x0000...0000) used to check for unset addresses
EthereumZeroAddress = "0x0000000000000000000000000000000000000000"
// EthereumAddressHexLength represents the length of an Ethereum address in hex characters (20 bytes * 2)
EthereumAddressHexLength = 40
// ENSResolverFunctionSelector is the function selector for ENS registry's resolver(bytes32) method
ENSResolverFunctionSelector = "0x0178b8bf"
// ENSAddrFunctionSelector is the function selector for the resolver's addr(bytes32) method
ENSAddrFunctionSelector = "0x3b3b57de"
// ENSExpirationFunctionSelector is the function selector for ENS registry's nameExpires(bytes32) method
ENSExpirationFunctionSelector = "0x1aa2e643"
// ENSBaseRegistrarAddress is needed for checking .eth domain expiration
ENSBaseRegistrarAddress = "0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85"
)
// Configuration represents json config file
type Configuration struct {
CoinName string `json:"coin_name"`
CoinShortcut string `json:"coin_shortcut"`
Network string `json:"network"`
RPCURL string `json:"rpc_url"`
RPCURLWS string `json:"rpc_url_ws"`
RPCTimeout int `json:"rpc_timeout"`
TraceTimeout string `json:"trace_timeout,omitempty"`
Erc20BatchSize int `json:"erc20_batch_size,omitempty"`
BlockAddressesToKeep int `json:"block_addresses_to_keep"`
HotAddressMinContracts int `json:"hot_address_min_contracts,omitempty"`
HotAddressLRUCacheSize int `json:"hot_address_lru_cache_size,omitempty"`
HotAddressMinHits int `json:"hot_address_min_hits,omitempty"`
AddressContractsCacheMinSize int `json:"address_contracts_cache_min_size,omitempty"`
AddressContractsCacheMaxBytes int64 `json:"address_contracts_cache_max_bytes,omitempty"`
AddressContractsCacheBulkMaxBytes int64 `json:"address_contracts_cache_bulk_max_bytes,omitempty"`
AddressAliases bool `json:"address_aliases,omitempty"`
MempoolTxTimeoutHours int `json:"mempoolTxTimeoutHours"`
MempoolTxTimeout string `json:"mempoolTxTimeout,omitempty"`
AlternativeMempoolTxTimeout string `json:"alternativeMempoolTxTimeout,omitempty"`
QueryBackendOnMempoolResync bool `json:"queryBackendOnMempoolResync"`
ProcessInternalTransactions bool `json:"processInternalTransactions"`
ProcessZeroInternalTransactions bool `json:"processZeroInternalTransactions"`
ConsensusNodeVersionURL string `json:"consensusNodeVersion"`
DisableMempoolSync bool `json:"disableMempoolSync,omitempty"`
Eip1559Fees bool `json:"eip1559Fees,omitempty"`
AlternativeEstimateFee string `json:"alternative_estimate_fee,omitempty"`
AlternativeEstimateFeeParams string `json:"alternative_estimate_fee_params,omitempty"`
// AverageBlockTimeMs is the chain's nominal block cadence in ms;
// required for EVM coins (translates duration settings to block counts).
AverageBlockTimeMs int `json:"averageBlockTimeMs,omitempty"`
// MissingBlockRetry overrides the sync-worker missing-block retry policy
// per chain. All fields are optional; missing fields use built-in defaults.
MissingBlockRetry *bchain.MissingBlockRetry `json:"missingBlockRetry,omitempty"`
}
func parseNonNegativeDuration(name string, value string) (time.Duration, error) {
d, err := time.ParseDuration(value)
if err != nil {
return 0, errors.Annotatef(err, "invalid %s", name)
}
if d < 0 {
return 0, errors.Errorf("%s must not be negative", name)
}
return d, nil
}
func parsePositiveDuration(name string, value string) (time.Duration, error) {
d, err := parseNonNegativeDuration(name, value)
if err != nil {
return 0, err
}
if d == 0 {
return 0, errors.Errorf("%s must be positive", name)
}
return d, nil
}
// MempoolTxTimeoutDuration returns the Blockbook-side EVM mempool retention.
func (c *Configuration) MempoolTxTimeoutDuration(alternativeSendTxProviderEnabled bool) (time.Duration, error) {
if c.MempoolTxTimeout != "" {
return parseNonNegativeDuration("mempoolTxTimeout", c.MempoolTxTimeout)
}
// Keep the shorter timeout scoped to alternative/private submission only.
if alternativeSendTxProviderEnabled {
return defaultMempoolTxTimeoutWithAlternativeProvider, nil
}
return time.Duration(c.MempoolTxTimeoutHours) * time.Hour, nil
}
// AlternativeMempoolTxTimeoutDuration returns the alternative-provider cache retention.
func (c *Configuration) AlternativeMempoolTxTimeoutDuration() (time.Duration, error) {
if c.AlternativeMempoolTxTimeout != "" {
return parsePositiveDuration("alternativeMempoolTxTimeout", c.AlternativeMempoolTxTimeout)
}
return defaultAlternativeMempoolTxTimeout, nil
}
// mempoolRetentionInverted reports whether the alternative-provider cache is configured to outlive
// the wrapped Blockbook mempool. Every cache exit clears the wrapped mempool too, but the mempool's
// own timeout sweep is the one exit that does NOT clear the cache: inverted, that sweep drops a
// private transaction's address index while the cache keeps serving its body as pending, and nothing
// reconciles the two. Only an explicit timeout pair can invert it; the defaults cannot.
func mempoolRetentionInverted(alternativeTimeout, mempoolTimeout time.Duration) bool {
return alternativeTimeout >= mempoolTimeout
}
// AverageBlockTimeDuration returns AverageBlockTimeMs as a time.Duration.
func (c *Configuration) AverageBlockTimeDuration() (time.Duration, error) {
if c.AverageBlockTimeMs <= 0 {
return 0, errors.Errorf("averageBlockTimeMs must be a positive integer")
}
return time.Duration(c.AverageBlockTimeMs) * time.Millisecond, nil
}
// EthereumRPC is an interface to JSON-RPC eth service.
type EthereumRPC struct {
*bchain.BaseChain
Client bchain.EVMClient
RPC bchain.EVMRPCClient
MainNetChainID Network
Timeout time.Duration
Parser EthereumLikeParser
PushHandler func(bchain.NotificationType)
OpenRPC func(string, string) (bchain.EVMRPCClient, bchain.EVMClient, error)
Mempool *bchain.MempoolEthereumType
mempoolInitialized bool
bestHeaderLock sync.Mutex
bestHeader bchain.EVMHeader
// newBlockNotifyCh coalesces bursts of newHeads events into a single wake-up.
// This keeps the subscription reader unblocked while we refresh the canonical tip.
newBlockNotifyCh chan struct{}
// subscribeReadersOnce guards the long-lived consumer goroutines (tip notifier,
// tip watchdog and the NewBlock/NewTx channel readers) so reconnectRPC ->
// subscribeEvents only re-creates the connection-bound subscriptions and never
// leaks a fresh set of readers on every reconnect.
subscribeReadersOnce sync.Once
// lastSubNotifyNs is the UnixNano of the last newHeads notification that
// advanced the cached tip (subscription path only, never watchdog polls).
// Keying liveness on tip advance, not mere arrival, lets the watchdog also
// catch a feed that keeps delivering but is stuck on one height.
lastSubNotifyNs atomic.Int64
NewBlock bchain.EVMNewBlockSubscriber
newBlockSubscription bchain.EVMClientSubscription
NewTx bchain.EVMNewTxSubscriber
newTxSubscription bchain.EVMClientSubscription
ChainConfig *Configuration
metrics *common.Metrics
supportedStakingPools []string
stakingPoolNames []string
stakingPoolContracts []string
alternativeFeeProvider alternativeFeeProviderInterface
alternativeSendTxProvider *AlternativeSendTxProvider
InternalDataProvider bchain.EthereumInternalDataProvider
consensusMonitor *consensusVersionMonitor
// Multicall3 deployment state; lazily probed on first call. See multicall.go.
multicall3Probe atomic.Int32
multicall3ProbeSF singleflight.Group
}
// NewEthereumRPC returns new EthRPC instance.
func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.NotificationType)) (bchain.BlockChain, error) {
var err error
var c Configuration
err = json.Unmarshal(config, &c)
if err != nil {
return nil, errors.Annotatef(err, "Invalid configuration file")
}
// keep at least 100 mappings block->addresses to allow rollback
if c.BlockAddressesToKeep < 100 {
c.BlockAddressesToKeep = 100
}
if c.Erc20BatchSize <= 0 {
c.Erc20BatchSize = defaultErc20BatchSize
}
if c.HotAddressMinContracts <= 0 {
c.HotAddressMinContracts = defaultHotAddressMinContracts
}
if c.HotAddressLRUCacheSize <= 0 {
c.HotAddressLRUCacheSize = defaultHotAddressLRUCacheSize
} else if c.HotAddressLRUCacheSize > maxHotAddressLRUCacheSize {
glog.Warningf("hot_address_lru_cache_size=%d is too large, clamping to %d", c.HotAddressLRUCacheSize, maxHotAddressLRUCacheSize)
c.HotAddressLRUCacheSize = maxHotAddressLRUCacheSize
}
if c.HotAddressMinHits <= 0 {
c.HotAddressMinHits = defaultHotAddressMinHits
} else if c.HotAddressMinHits > maxHotAddressMinHits {
glog.Warningf("hot_address_min_hits=%d is too large, clamping to %d", c.HotAddressMinHits, maxHotAddressMinHits)
c.HotAddressMinHits = maxHotAddressMinHits
}
if c.AddressContractsCacheMinSize <= 0 {
c.AddressContractsCacheMinSize = defaultAddressContractsCacheMinSize
}
if c.AddressContractsCacheMaxBytes <= 0 {
c.AddressContractsCacheMaxBytes = defaultAddressContractsCacheMaxBytes
}
if c.AddressContractsCacheBulkMaxBytes <= 0 {
c.AddressContractsCacheBulkMaxBytes = defaultAddressContractsCacheBulkMaxBytes
}
if c.AddressContractsCacheBulkMaxBytes < c.AddressContractsCacheMaxBytes {
glog.Warningf("address_contracts_cache_bulk_max_bytes=%d is less than address_contracts_cache_max_bytes=%d", c.AddressContractsCacheBulkMaxBytes, c.AddressContractsCacheMaxBytes)
}
if c.TraceTimeout != "" {
if _, err := time.ParseDuration(c.TraceTimeout); err != nil {
return nil, errors.Annotatef(err, "invalid trace_timeout")
}
}
if _, err := c.MempoolTxTimeoutDuration(false); err != nil {
return nil, err
}
if _, err := c.AlternativeMempoolTxTimeoutDuration(); err != nil {
return nil, err
}
if _, err := c.AverageBlockTimeDuration(); err != nil {
return nil, err
}
s := &EthereumRPC{
BaseChain: &bchain.BaseChain{},
ChainConfig: &c,
}
// 1-slot buffer ensures we only queue one "refresh tip" signal at a time.
s.newBlockNotifyCh = make(chan struct{}, 1)
bchain.ProcessInternalTransactions = c.ProcessInternalTransactions
// always create parser
parser := NewEthereumParser(c.BlockAddressesToKeep, c.AddressAliases)
parser.HotAddressMinContracts = c.HotAddressMinContracts
parser.HotAddressLRUCacheSize = c.HotAddressLRUCacheSize
parser.HotAddressMinHits = c.HotAddressMinHits
parser.AddrContractsCacheMinSize = c.AddressContractsCacheMinSize
parser.AddrContractsCacheMaxBytes = c.AddressContractsCacheMaxBytes
parser.AddrContractsCacheBulkMaxBytes = c.AddressContractsCacheBulkMaxBytes
s.Parser = parser
if c.RPCTimeout <= 0 {
glog.Warningf("rpc_timeout=%d is invalid, using default %d seconds", c.RPCTimeout, defaultRPCTimeoutSeconds)
c.RPCTimeout = defaultRPCTimeoutSeconds
}
s.Timeout = time.Duration(c.RPCTimeout) * time.Second
s.PushHandler = pushHandler
return s, nil
}
// SetMetrics sets the metrics registry. The alternative send-tx provider receives the same metrics
// at construction (NewAlternativeSendTxProvider, called from InitAlternativeProviders, which runs
// after SetMetrics), so it is intentionally not assigned here - and must not be, since its reconcile
// goroutine reads provider.metrics without synchronization, so that field stays write-once.
func (b *EthereumRPC) SetMetrics(metrics *common.Metrics) {
b.metrics = metrics
}
// AverageBlockTimeDuration exposes the chain's nominal block cadence.
func (b *EthereumRPC) AverageBlockTimeDuration() (time.Duration, error) {
return b.ChainConfig.AverageBlockTimeDuration()
}
// MissingBlockRetryOverride exposes the per-chain sync-worker retry override
// (or nil to use built-in defaults). Consumed by blockbook.go at SyncWorker
// construction via a duck-typed interface assertion.
func (b *EthereumRPC) MissingBlockRetryOverride() *bchain.MissingBlockRetry {
if b.ChainConfig == nil {
return nil
}
return b.ChainConfig.MissingBlockRetry
}
func (b *EthereumRPC) observeEthCall(mode string, count int) {
if b.metrics == nil || count <= 0 {
return
}
b.metrics.EthCallRequests.With(common.Labels{"mode": mode}).Add(float64(count))
}
// ObserveChainDataFallback increments a metric for chain-data fallback paths.
func (b *EthereumRPC) ObserveChainDataFallback(component, reason string) {
if b.metrics == nil || component == "" || reason == "" {
return
}
b.metrics.ChainDataFallbacks.With(common.Labels{"component": component, "reason": reason}).Inc()
}
func (b *EthereumRPC) observeEthCallError(mode, errType string) {
if b.metrics == nil {
return
}
b.metrics.EthCallErrors.With(common.Labels{"mode": mode, "type": errType}).Inc()
}
func (b *EthereumRPC) observeEthCallBatch(size int) {
if b.metrics == nil || size <= 0 {
return
}
b.metrics.EthCallBatchSize.Observe(float64(size))
}
func (b *EthereumRPC) observeEthCallContractInfo(field string) {
if b.metrics == nil {
return
}
b.metrics.EthCallContractInfo.With(common.Labels{"field": field}).Inc()
}
func (b *EthereumRPC) observeEthCallTokenURI(method string) {
if b.metrics == nil {
return
}
b.metrics.EthCallTokenURI.With(common.Labels{"method": method}).Inc()
}
func (b *EthereumRPC) observeEthCallStakingPool(field string) {
if b.metrics == nil {
return
}
b.metrics.EthCallStakingPool.With(common.Labels{"field": field}).Inc()
}
func ethSyncRpcErrStatus(err error) string {
if stdErrors.Is(err, context.DeadlineExceeded) {
return "timeout"
}
var httpErr rpc.HTTPError
if stdErrors.As(err, &httpErr) {
switch {
case httpErr.StatusCode >= 500:
return "http_5xx"
case httpErr.StatusCode >= 400:
return "http_4xx"
default:
return "http_other"
}
}
var rpcErr rpc.Error
if stdErrors.As(err, &rpcErr) {
return "rpc_" + strconv.Itoa(rpcErr.ErrorCode())
}
return "error"
}
func (b *EthereumRPC) observeEthSyncRpcError(method string, err error) {
if b.metrics == nil || err == nil {
return
}
b.metrics.EthSyncRpcErrors.With(common.Labels{"method": method, "status": ethSyncRpcErrStatus(err)}).Inc()
}
func (b *EthereumRPC) observeSyncRPCLatency(method string, start time.Time, err error) {
if b.metrics == nil {
return
}
errorLabel := ""
if err != nil {
errorLabel = "failure"
}
b.metrics.RPCSyncLatency.With(common.Labels{"method": method, "error": errorLabel}).Observe(float64(time.Since(start)) / 1e6)
}
// EnsureSameRPCHost validates both RPC URLs and logs a warning if hosts differ.
func EnsureSameRPCHost(httpURL, wsURL string) error {
if httpURL == "" || wsURL == "" {
return nil
}
httpHost, err := rpcURLHost(httpURL)
if err != nil {
return errors.Annotatef(err, "rpc_url")
}
wsHost, err := rpcURLHost(wsURL)
if err != nil {
return errors.Annotatef(err, "rpc_url_ws")
}
if !strings.EqualFold(httpHost, wsHost) {
glog.Warningf("rpc_url host %q and rpc_url_ws host %q differ", httpHost, wsHost)
}
return nil
}
// NormalizeRPCURLs validates HTTP and WS RPC endpoints and enforces same-host rules.
func NormalizeRPCURLs(httpURL, wsURL string) (string, string, error) {
callURL := strings.TrimSpace(httpURL)
subURL := strings.TrimSpace(wsURL)
if callURL == "" {
return "", "", errors.New("rpc_url is empty")
}
if subURL == "" {
return "", "", errors.New("rpc_url_ws is empty")
}
if err := validateRPCURLScheme(callURL, "rpc_url", []string{"http", "https"}); err != nil {
return "", "", err
}
if err := validateRPCURLScheme(subURL, "rpc_url_ws", []string{"ws", "wss"}); err != nil {
return "", "", err
}
if err := EnsureSameRPCHost(callURL, subURL); err != nil {
return "", "", err
}
return callURL, subURL, nil
}
func validateRPCURLScheme(rawURL, field string, allowedSchemes []string) error {
parsed, err := url.Parse(rawURL)
if err != nil {
return errors.Annotatef(err, "%s", field)
}
scheme := strings.ToLower(parsed.Scheme)
if scheme == "" {
return errors.Errorf("%s missing scheme in %q", field, rawURL)
}
for _, allowed := range allowedSchemes {
if scheme == allowed {
return nil
}
}
return errors.Errorf("%s must use %s scheme: %q", field, strings.Join(allowedSchemes, " or "), rawURL)
}
func rpcURLHost(rawURL string) (string, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", err
}
host := parsed.Hostname()
if host == "" {
return "", errors.Errorf("missing host in %q", rawURL)
}
return host, nil
}
// dialTimeout bounds the initial RPC/WS handshake. A websocket backend behind a
// load balancer can accept the TCP socket but never complete the upgrade — the
// exact silent stall tipWatchdog exists to heal. Dialing with context.Background()
// then blocks forever, and because reconnectRPC runs on the lone tipWatchdog
// goroutine that single healer parks indefinitely: the cached tip stays frozen,
// resyncIndex keeps reporting a false syncNotNeeded, and sync silently stalls until
// a restart. go-ethereum uses this context only for the first handshake, so the
// established connection's lifetime is unaffected. A var so tests can shorten it.
var dialTimeout = 30 * time.Second
func dialRPC(rawURL string) (*rpc.Client, error) {
if rawURL == "" {
return nil, errors.New("empty rpc url")
}
opts := []rpc.ClientOption{}
if strings.HasPrefix(rawURL, "ws://") || strings.HasPrefix(rawURL, "wss://") {
opts = append(opts, rpc.WithWebsocketMessageSizeLimit(0))
}
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
defer cancel()
return rpc.DialOptions(ctx, rawURL, opts...)
}
// OpenRPC opens RPC connection to ETH backend.
var OpenRPC = func(httpURL, wsURL string) (bchain.EVMRPCClient, bchain.EVMClient, error) {
callURL, subURL, err := NormalizeRPCURLs(httpURL, wsURL)
if err != nil {
return nil, nil, err
}
callClient, err := dialRPC(callURL)
if err != nil {
return nil, nil, err
}
subClient := callClient
if subURL != callURL {
subClient, err = dialRPC(subURL)
if err != nil {
callClient.Close()
return nil, nil, err
}
}
rc := &DualRPCClient{CallClient: callClient, SubClient: subClient}
ec := &EthereumClient{Client: ethclient.NewClient(callClient)}
return rc, ec, nil
}
// Initialize initializes ethereum rpc interface
func (b *EthereumRPC) Initialize() error {
b.OpenRPC = OpenRPC
rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL, b.ChainConfig.RPCURLWS)
if err != nil {
return err
}
// set chain specific
b.Client = ec
b.RPC = rc
b.MainNetChainID = MainNet
b.NewBlock = &EthereumNewBlock{channel: make(chan *types.Header)}
b.NewTx = &EthereumNewTx{channel: make(chan ethcommon.Hash)}
ctx, cancel := context.WithTimeout(context.Background(), b.Timeout)
defer cancel()
id, err := b.Client.NetworkID(ctx)
if err != nil {
return err
}
// parameters for getInfo request
switch Network(id.Uint64()) {
case MainNet:
b.Testnet = false
b.Network = "livenet"
case TestNetSepolia:
b.Testnet = true
b.Network = "sepolia"
case TestNetHoodi:
b.Testnet = true
b.Network = "hoodi"
default:
return errors.Errorf("Unknown network id %v", id)
}
err = b.initStakingPools()
if err != nil {
return err
}
if err = b.InitAlternativeProviders(); err != nil {
return err
}
b.consensusMonitor = newConsensusVersionMonitor(b.ChainConfig.ConsensusNodeVersionURL)
b.consensusMonitor.start()
glog.Info("rpc: block chain ", b.Network)
return nil
}
const (
consensusVersionUnreachable = "unreachable-locally"
consensusVersionPollPeriod = 60 * time.Second
)
// consensusVersionMonitor probes the configured consensus node /eth/v1/node/version
// endpoint and caches the latest result. The cached value (real version or
// "unreachable-locally") is the signal exposed via getInfo and the Prometheus
// backend_subversion label; periodic re-probes are silent so a node being
// down does not spam the log.
type consensusVersionMonitor struct {
url string
mu sync.RWMutex
version string
stop chan struct{}
stopOnce sync.Once
}
func newConsensusVersionMonitor(url string) *consensusVersionMonitor {
if url == "" {
return nil
}
return &consensusVersionMonitor{url: url, stop: make(chan struct{})}
}
// start performs an initial synchronous probe (logging one WARN if it fails)
// and then launches a background goroutine that re-probes every
// consensusVersionPollPeriod. Safe to call on a nil receiver.
func (m *consensusVersionMonitor) start() {
if m == nil {
return
}
v, err := m.fetch()
if err != nil {
glog.Warningf("consensus node version probe failed for %s: %v", m.url, err)
v = consensusVersionUnreachable
}
m.set(v)
go m.run()
}
func (m *consensusVersionMonitor) run() {
ticker := time.NewTicker(consensusVersionPollPeriod)
defer ticker.Stop()
for {
select {
case <-m.stop:
return
case <-ticker.C:
v, err := m.fetch()
if err != nil {
v = consensusVersionUnreachable
}
m.set(v)
}
}
}
func (m *consensusVersionMonitor) fetch() (string, error) {
httpClient := &http.Client{Timeout: 2 * time.Second}
resp, err := httpClient.Get(m.url)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
var v struct {
Data struct {
Version string `json:"version"`
} `json:"data"`
}
if err := json.Unmarshal(body, &v); err != nil {
return "", err
}
return v.Data.Version, nil
}
func (m *consensusVersionMonitor) set(v string) {
m.mu.Lock()
m.version = v
m.mu.Unlock()
}
func (m *consensusVersionMonitor) get() string {
if m == nil {
return ""
}
m.mu.RLock()
defer m.mu.RUnlock()
return m.version
}
func (m *consensusVersionMonitor) shutdown() {
if m == nil {
return
}
m.stopOnce.Do(func() { close(m.stop) })
}
// InitAlternativeProviders initializes alternative providers
func (b *EthereumRPC) InitAlternativeProviders() error {
if err := b.initAlternativeFeeProvider(); err != nil {
return err
}
// Env prefix follows explicit network aliases such as OP/BASE, otherwise ETH.
network := b.ChainConfig.Network
if network == "" {
network = b.ChainConfig.CoinShortcut
}
alternativeMempoolTxTimeout, err := b.ChainConfig.AlternativeMempoolTxTimeoutDuration()
if err != nil {
return err
}
b.alternativeSendTxProvider = NewAlternativeSendTxProvider(network, b.ChainConfig.RPCTimeout, alternativeMempoolTxTimeout, b.metrics)
return nil
}
// CreateMempool creates mempool if not already created, however does not initialize it
func (b *EthereumRPC) CreateMempool(chain bchain.BlockChain) (bchain.Mempool, error) {
if b.Mempool == nil {
mempoolTxTimeout, err := b.ChainConfig.MempoolTxTimeoutDuration(b.alternativeSendTxProvider != nil)
if err != nil {
return nil, err
}
b.Mempool = bchain.NewMempoolEthereumType(chain, mempoolTxTimeout, b.ChainConfig.QueryBackendOnMempoolResync)
glog.Info("mempool created, MempoolTxTimeout=", mempoolTxTimeout, ", QueryBackendOnMempoolResync=", b.ChainConfig.QueryBackendOnMempoolResync, ", DisableMempoolSync=", b.ChainConfig.DisableMempoolSync)
if b.alternativeSendTxProvider != nil {
// warned here, not in Validate: the effective mempool retention depends on the
// env-configured provider existing
if mempoolRetentionInverted(b.alternativeSendTxProvider.mempoolTxsTimeout, mempoolTxTimeout) {
glog.Warningf("alternativeMempoolTxTimeout=%s is not shorter than mempoolTxTimeout=%s: the wrapped mempool may drop a private transaction's address index while the provider cache still serves it as pending", b.alternativeSendTxProvider.mempoolTxsTimeout, mempoolTxTimeout)
}
b.alternativeSendTxProvider.SetupMempool(b.Mempool, b.removeTransactionFromMempool)
}
}
return b.Mempool, nil
}
// InitializeMempool creates subscriptions to newHeads and newPendingTransactions
func (b *EthereumRPC) InitializeMempool(addrDescForOutpoint bchain.AddrDescForOutpointFunc, onNewTx bchain.OnNewTxFunc) error {
if b.Mempool == nil {
return errors.New("Mempool not created")
}
var err error
var txs []string
// get initial mempool transactions
// workaround for an occasional `decoding block` error from getBlockRaw - try 3 times with a delay and then proceed
for i := 0; i < 3; i++ {
txs, err = b.GetMempoolTransactions()
if err == nil {
break
}
glog.Error("GetMempoolTransaction ", err)
time.Sleep(time.Second * 5)
}
for _, txid := range txs {
b.Mempool.AddTransactionToMempool(txid)
}
b.Mempool.OnNewTx = onNewTx
if err = b.subscribeEvents(); err != nil {
return err
}
b.mempoolInitialized = true
return nil
}
func (b *EthereumRPC) subscribeEvents() error {
// The tip notifier, tip watchdog and the NewBlock/NewTx channel readers bind to
// the persistent channels, not to a specific connection, so start them exactly
// once. reconnectRPC -> subscribeEvents then only re-creates the EthSubscribe
// bound subscriptions below, instead of leaking a fresh reader set per reconnect.
b.subscribeReadersOnce.Do(func() {
go b.newBlockNotifier()
go b.tipWatchdog()
// new block notifications handling
go func() {
for {
h, ok := b.NewBlock.Read()
if !ok {
break
}
// Advance the tip from the delivered header, not a re-query over
// the load-balanced HTTP path (see onFeedHeader).
b.onFeedHeader(h)
}
}()
// new mempool transaction notifications handling
if !b.ChainConfig.DisableMempoolSync {
go func() {
for {
t, ok := b.NewTx.Read()
if !ok {
break
}
hex := t.Hex()
if glog.V(2) {
glog.Info("rpc: new tx ", hex)
}
added := b.Mempool.AddTransactionToMempool(hex)
if added {
b.PushHandler(bchain.NotificationNewTx)
}
}
}()
}
})
// new block subscription - re-created on every (re)connect
if err := b.subscribe("newHeads", func() (bchain.EVMClientSubscription, error) {
// invalidate the previous subscription - it is either the first one or there was an error
b.newBlockSubscription = nil
ctx, cancel := context.WithTimeout(context.Background(), b.Timeout)
defer cancel()
sub, err := b.RPC.EthSubscribe(ctx, b.NewBlock.Channel(), "newHeads")
if err != nil {
return nil, errors.Annotatef(err, "EthSubscribe newHeads")
}
b.newBlockSubscription = sub
glog.Info("Subscribed to newHeads")
return sub, nil
}); err != nil {
return err
}
// Arm lastSubNotifyNs at subscribe time, not only on the first tip advance.
// Liveness is otherwise stamped only when a header advances the tip, so a
// subscription that never delivers a usable header leaves it at 0 and keeps
// tipWatchdog's lastNs == 0 gate closed forever: the cached tip never refreshes
// and resyncIndex reports a silent syncNotNeeded. Seeding here lets a stalled
// feed age past the threshold so the watchdog polls and reconnects.
b.markSubscriptionAlive()
if !b.ChainConfig.DisableMempoolSync {
// new mempool transaction subscription - re-created on every (re)connect
if err := b.subscribe("newPendingTransactions", func() (bchain.EVMClientSubscription, error) {
// invalidate the previous subscription - it is either the first one or there was an error
b.newTxSubscription = nil
ctx, cancel := context.WithTimeout(context.Background(), b.Timeout)
defer cancel()
sub, err := b.RPC.EthSubscribe(ctx, b.NewTx.Channel(), "newPendingTransactions")
if err != nil {
return nil, errors.Annotatef(err, "EthSubscribe newPendingTransactions")
}
b.newTxSubscription = sub
glog.Info("Subscribed to newPendingTransactions")
return sub, nil
}); err != nil {
return err
}
}
return nil
}
// subscribe subscribes notification and tries to resubscribe in case of error
func (b *EthereumRPC) subscribe(name string, f func() (bchain.EVMClientSubscription, error)) error {
s, err := f()
if err != nil {
return err
}
go func() {
Loop:
for {
// wait for error in subscription
e := <-s.Err()
// nil error means sub.Unsubscribe called, exit goroutine
if e == nil {
return
}
glog.Error("Subscription error ", name, ": ", e)
b.ObserveSubscriptionEvent(name, "error")
timer := time.NewTimer(time.Second * 2)
// try in 2 second interval to resubscribe
for {
select {
case e = <-s.Err():
if e == nil {
return
}
case <-timer.C:
ns, err := f()
if err == nil {
// subscription successful, restart wait for next error
b.ObserveSubscriptionEvent(name, "resubscribed")
s = ns
continue Loop
}
glog.Error("Resubscribe error ", name, ": ", err)
b.ObserveSubscriptionEvent(name, "resubscribe_failed")
timer.Reset(time.Second * 2)
}
}
}
}()
return nil
}
// initAlternativeFeeProvider sets up the configured EVM alternative fee provider.
// When a provider is explicitly selected in the coin config but cannot be
// constructed (for example a required API-key env var such as INFURA_API_KEY is
// missing), the error is returned so startup fails fast rather than silently
// reverting to default fee estimation.
func (b *EthereumRPC) initAlternativeFeeProvider() error {
var err error
if b.ChainConfig.AlternativeEstimateFee == "1inch" {
if b.alternativeFeeProvider, err = NewOneInchFeesProvider(b, b.ChainConfig.AlternativeEstimateFeeParams, b.metrics); err != nil {
b.alternativeFeeProvider = nil
return err
}
} else if b.ChainConfig.AlternativeEstimateFee == "infura" {
if b.alternativeFeeProvider, err = NewInfuraFeesProvider(b, b.ChainConfig.AlternativeEstimateFeeParams, b.metrics); err != nil {
b.alternativeFeeProvider = nil
return err
}
}
if b.alternativeFeeProvider != nil {
glog.Info("Using alternative fee provider ", b.ChainConfig.AlternativeEstimateFee)
}
return nil
}
func (b *EthereumRPC) closeRPC() {
if b.newBlockSubscription != nil {
b.newBlockSubscription.Unsubscribe()
}
if b.newTxSubscription != nil {
b.newTxSubscription.Unsubscribe()
}
if b.RPC != nil {
b.RPC.Close()
}
}
// CloseRPC closes the underlying RPC client, aborting any in-flight calls.
// Exported so embedders (e.g. Tron) can abort sync RPCs on shutdown without
// running the EVM-specific subscription/monitor teardown done by Shutdown.
func (b *EthereumRPC) CloseRPC() {
b.closeRPC()
}
func (b *EthereumRPC) reconnectRPC() error {
glog.Info("Reconnecting RPC")
b.closeRPC()
rc, ec, err := b.OpenRPC(b.ChainConfig.RPCURL, b.ChainConfig.RPCURLWS)
if err != nil {
return err
}
b.RPC = rc
b.Client = ec
return b.subscribeEvents()
}
// Shutdown cleans up rpc interface to ethereum
func (b *EthereumRPC) Shutdown(ctx context.Context) error {
b.closeRPC()
b.NewBlock.Close()
b.NewTx.Close()
b.consensusMonitor.shutdown()
b.alternativeSendTxProvider.shutdown()
glog.Info("rpc: shutdown")
return nil
}
// GetCoinName returns coin name
func (b *EthereumRPC) GetCoinName() string {
return b.ChainConfig.CoinName
}
// GetSubversion returns empty string, ethereum does not have subversion
func (b *EthereumRPC) GetSubversion() string {
return ""
}
// GetChainInfo returns information about the connected backend
func (b *EthereumRPC) GetChainInfo() (*bchain.ChainInfo, error) {
h, err := b.getBestHeader()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), b.Timeout)
defer cancel()
netStart := time.Now()
id, err := b.Client.NetworkID(ctx)
b.observeSyncRPCLatency("net_version", netStart, err)
if err != nil {
return nil, err
}
var ver string
web3Start := time.Now()
err = b.RPC.CallContext(ctx, &ver, "web3_clientVersion")
b.observeSyncRPCLatency("web3_clientVersion", web3Start, err)
if err != nil {
return nil, err
}
rv := &bchain.ChainInfo{
Blocks: int(h.Number().Int64()),
Bestblockhash: h.Hash(),
Difficulty: h.Difficulty().String(),