From 1d787280c979a6e6835c007eb08b76fecb9b8a75 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Wed, 25 Jan 2023 14:32:42 +0900 Subject: [PATCH] client/eth: Check provider header times. When fetching a new or cached header with a provider, do a basic check on the header's time to determine if the header, and so the provider, are up to date. --- client/asset/eth/eth.go | 12 +- client/asset/eth/eth_test.go | 94 +++++----- client/asset/eth/multirpc.go | 184 ++++++++++---------- client/asset/eth/multirpc_live_test.go | 19 +- client/asset/eth/nodeclient.go | 11 +- client/asset/eth/nodeclient_harness_test.go | 38 ++-- 6 files changed, 189 insertions(+), 169 deletions(-) diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index e51bbf159c..4c69e49240 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -295,7 +295,7 @@ type ethFetcher interface { sendSignedTransaction(ctx context.Context, tx *types.Transaction) error sendTransaction(ctx context.Context, txOpts *bind.TransactOpts, to common.Address, data []byte) (*types.Transaction, error) signData(data []byte) (sig, pubKey []byte, err error) - syncProgress(context.Context) (*ethereum.SyncProgress, error) + syncProgress(context.Context) (progress *ethereum.SyncProgress, bestHeaderUNIXTime uint64, err error) transactionConfirmations(context.Context, common.Hash) (uint32, error) getTransaction(context.Context, common.Hash) (*types.Transaction, int64, error) txOpts(ctx context.Context, val, maxGas uint64, maxFeeRate, nonce *big.Int) (*bind.TransactOpts, error) @@ -2798,18 +2798,14 @@ func (*baseWallet) ValidateSecret(secret, secretHash []byte) bool { // more, requesting the best block header starts to fail after a few tries // during initial sync. Investigate how to get correct sync progress. func (eth *baseWallet) SyncStatus() (bool, float32, error) { - prog, err := eth.node.syncProgress(eth.ctx) + prog, bestHeaderUNIXTime, err := eth.node.syncProgress(eth.ctx) if err != nil { return false, 0, err } checkHeaderTime := func() (bool, error) { - bh, err := eth.node.bestHeader(eth.ctx) - if err != nil { - return false, err - } // Time in the header is in seconds. - timeDiff := time.Now().Unix() - int64(bh.Time) - if timeDiff > dexeth.MaxBlockInterval && eth.net != dex.Simnet { + timeDiff := time.Now().Unix() - int64(bestHeaderUNIXTime) + if timeDiff > dexeth.MaxBlockInterval { eth.log.Infof("Time since last eth block (%d sec) exceeds %d sec."+ "Assuming not in sync. Ensure your computer's system clock "+ "is correct.", timeDiff, dexeth.MaxBlockInterval) diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 5d386787eb..de29b60c13 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -91,43 +91,45 @@ type tGetTxRes struct { } type testNode struct { - acct *accounts.Account - addr common.Address - connectErr error - bestHdr *types.Header - bestHdrErr error - syncProg ethereum.SyncProgress - bal *big.Int - balErr error - signDataErr error - privKey *ecdsa.PrivateKey - swapVers map[uint32]struct{} // For SwapConfirmations -> swap. TODO for other contractor methods - swapMap map[[32]byte]*dexeth.SwapState - refundable bool - baseFee *big.Int - tip *big.Int - netFeeStateErr error - confNonce uint64 - confNonceErr error - getTxRes *types.Transaction - getTxResMap map[common.Hash]*tGetTxRes - getTxHeight int64 - getTxErr error - receipt *types.Receipt - receiptTx *types.Transaction - receiptErr error - hdrByHash *types.Header - txReceipt *types.Receipt - lastSignedTx *types.Transaction - sendTxTx *types.Transaction - sendTxErr error - simBackend bind.ContractBackend - maxFeeRate *big.Int - pendingTxs []*types.Transaction - tContractor *tContractor - tokenContractor *tTokenContractor - contractor contractor - tokenParent *assetWallet // only set for tokens + acct *accounts.Account + addr common.Address + connectErr error + bestHdr *types.Header + bestHdrErr error + syncProg ethereum.SyncProgress + syncProgBestHeaderUNIXTime uint64 + syncProgErr error + bal *big.Int + balErr error + signDataErr error + privKey *ecdsa.PrivateKey + swapVers map[uint32]struct{} // For SwapConfirmations -> swap. TODO for other contractor methods + swapMap map[[32]byte]*dexeth.SwapState + refundable bool + baseFee *big.Int + tip *big.Int + netFeeStateErr error + confNonce uint64 + confNonceErr error + getTxRes *types.Transaction + getTxResMap map[common.Hash]*tGetTxRes + getTxHeight int64 + getTxErr error + receipt *types.Receipt + receiptTx *types.Transaction + receiptErr error + hdrByHash *types.Header + txReceipt *types.Receipt + lastSignedTx *types.Transaction + sendTxTx *types.Transaction + sendTxErr error + simBackend bind.ContractBackend + maxFeeRate *big.Int + pendingTxs []*types.Transaction + tContractor *tContractor + tokenContractor *tTokenContractor + contractor contractor + tokenParent *assetWallet // only set for tokens } func newBalance(current, in, out uint64) *Balance { @@ -206,8 +208,8 @@ func (n *testNode) lock() error { func (n *testNode) locked() bool { return false } -func (n *testNode) syncProgress(context.Context) (*ethereum.SyncProgress, error) { - return &n.syncProg, nil +func (n *testNode) syncProgress(context.Context) (prog *ethereum.SyncProgress, bestBlockUNIXTime uint64, err error) { + return &n.syncProg, n.syncProgBestHeaderUNIXTime, n.syncProgErr } func (n *testNode) peerCount() uint32 { return 1 @@ -479,8 +481,8 @@ func TestSyncStatus(t *testing.T) { tests := []struct { name string syncProg ethereum.SyncProgress + syncProgErr error subSecs uint64 - bestHdrErr error wantErr, wantSynced bool wantRatio float32 }{{ @@ -506,22 +508,22 @@ func TestSyncStatus(t *testing.T) { }, subSecs: dexeth.MaxBlockInterval + 1, }, { - name: "best header error", - bestHdrErr: errors.New(""), + name: "sync progress error", syncProg: ethereum.SyncProgress{ CurrentBlock: 25, HighestBlock: 0, }, - wantErr: true, + syncProgErr: errors.New(""), + wantErr: true, }} for _, test := range tests { nowInSecs := uint64(time.Now().Unix()) ctx, cancel := context.WithCancel(context.Background()) node := &testNode{ - syncProg: test.syncProg, - bestHdr: &types.Header{Time: nowInSecs - test.subSecs}, - bestHdrErr: test.bestHdrErr, + syncProg: test.syncProg, + syncProgBestHeaderUNIXTime: nowInSecs - test.subSecs, + syncProgErr: test.syncProgErr, } eth := &baseWallet{ node: node, diff --git a/client/asset/eth/multirpc.go b/client/asset/eth/multirpc.go index 1327fcbb0d..3f85ccc048 100644 --- a/client/asset/eth/multirpc.go +++ b/client/asset/eth/multirpc.go @@ -113,7 +113,9 @@ func (p *provider) cachedTip() *types.Header { p.tip.RLock() defer p.tip.RUnlock() - if time.Since(p.tip.failStamp) < failQuarantine || time.Since(p.tip.headerStamp) > stale { + if time.Since(p.tip.failStamp) < failQuarantine || + time.Since(p.tip.headerStamp) > stale || + time.Now().Unix()-int64(p.tip.header.Time) > dexeth.MaxBlockInterval { return nil } return p.tip.header @@ -138,27 +140,30 @@ func (p *provider) failed() bool { // bestHeader get the best known header from the provider, cached if available, // otherwise a new RPC call is made. func (p *provider) bestHeader(ctx context.Context, log dex.Logger) (*types.Header, error) { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() // Check if we have a cached header. if tip := p.cachedTip(); tip != nil { - log.Tracef("using cached header from %q", p.host) + log.Tracef("Using cached header from %q", p.host) return tip, nil } - log.Tracef("fetching fresh header from %q", p.host) + log.Tracef("Fetching fresh header from %q", p.host) hdr, err := p.ec.HeaderByNumber(ctx, nil /* latest */) if err != nil { p.setFailed() return nil, fmt.Errorf("HeaderByNumber error: %w", err) } + timeDiff := time.Now().Unix() - int64(hdr.Time) + if timeDiff > dexeth.MaxBlockInterval { + p.setFailed() + return nil, fmt.Errorf("time since last eth block (%d sec) exceeds %d sec. "+ + "Assuming provider %s is not in sync. Ensure your computer's system clock "+ + "is correct.", timeDiff, dexeth.MaxBlockInterval, p.host) + } p.setTip(hdr) return hdr, nil } func (p *provider) headerByHash(ctx context.Context, h common.Hash) (*types.Header, error) { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() hdr, err := p.ec.HeaderByHash(ctx, h) if err != nil { p.setFailed() @@ -170,8 +175,6 @@ func (p *provider) headerByHash(ctx context.Context, h common.Hash) (*types.Head // suggestTipCap returns a tip cap suggestion, cached if available, otherwise a // new RPC call is made. func (p *provider) suggestTipCap(ctx context.Context, log dex.Logger) *big.Int { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() if cachedV := p.tipCapV.Load(); cachedV != nil { rec := cachedV.(*cachedTipCap) if time.Since(rec.stamp) < tipCapSuggestionExpiration { @@ -634,9 +637,7 @@ func (m *multiRPCClient) transactionReceipt(ctx context.Context, txHash common.H } // Fetch a fresh one. - if err = m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + if err = m.withPreferred(ctx, func(ctx context.Context, p *provider) error { r, err = p.ec.TransactionReceipt(ctx, txHash) return err }); err != nil { @@ -685,8 +686,6 @@ func (tx *rpcTransaction) UnmarshalJSON(b []byte) error { } func getRPCTransaction(ctx context.Context, p *provider, txHash common.Hash) (*rpcTransaction, error) { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() var resp *rpcTransaction err := p.ec.rpc.CallContext(ctx, &resp, "eth_getTransactionByHash", txHash) if err != nil { @@ -703,7 +702,7 @@ func getRPCTransaction(ctx context.Context, p *provider, txHash common.Hash) (*r } func (m *multiRPCClient) getTransaction(ctx context.Context, txHash common.Hash) (tx *types.Transaction, h int64, err error) { - return tx, h, m.withPreferred(func(p *provider) error { + return tx, h, m.withPreferred(ctx, func(ctx context.Context, p *provider) error { resp, err := getRPCTransaction(ctx, p, txHash) if err != nil { if isNotFoundError(err) { @@ -726,9 +725,7 @@ func (m *multiRPCClient) getTransaction(ctx context.Context, txHash common.Hash) } func (m *multiRPCClient) getConfirmedNonce(ctx context.Context, blockNumber int64) (n uint64, err error) { - return n, m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return n, m.withPreferred(ctx, func(ctx context.Context, p *provider) error { n, err = p.ec.PendingNonceAt(ctx, m.address()) return err }) @@ -784,11 +781,26 @@ func errorFilter(err error, matches ...interface{}) bool { } // withOne runs the provider function against the providers in order until one -// succeeds or all have failed. -func (m *multiRPCClient) withOne(providers []*provider, f func(*provider) error, acceptabilityFilters ...acceptabilityFilter) (superError error) { +// succeeds or all have failed. The context used to run functions has a time +// limit equal to defaultRequestTimeout for all requests to return. If +// operations are expected to run longer than that the calling function should +// not use the altered context. +func (m *multiRPCClient) withOne(ctx context.Context, providers []*provider, f func(context.Context, *provider) error, acceptabilityFilters ...acceptabilityFilter) (superError error) { readyProviders := make([]*provider, 0, len(providers)) for _, p := range providers { if !p.failed() { + ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) + // Fetching the best header will check that either the + // provider's cached header is not too old or that a + // newly fetched header is not too old. If it is too + // old that indicates the provider is not in sync and + // should not be used. + _, err := p.bestHeader(ctx, m.log) + cancel() + if err != nil { + m.log.Warnf("Problem getting best header: %s.", err) + continue + } readyProviders = append(readyProviders, p) } } @@ -798,7 +810,9 @@ func (m *multiRPCClient) withOne(providers []*provider, f func(*provider) error, readyProviders = providers } for _, p := range readyProviders { - err := f(p) + ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) + err := f(ctx, p) + cancel() if err == nil { break } @@ -825,16 +839,16 @@ func (m *multiRPCClient) withOne(providers []*provider, f func(*provider) error, // withAny runs the provider function against known providers in random order // until one succeeds or all have failed. -func (m *multiRPCClient) withAny(f func(*provider) error, acceptabilityFilters ...acceptabilityFilter) error { +func (m *multiRPCClient) withAny(ctx context.Context, f func(context.Context, *provider) error, acceptabilityFilters ...acceptabilityFilter) error { providers := m.providerList() shuffleProviders(providers) - return m.withOne(providers, f, acceptabilityFilters...) + return m.withOne(ctx, providers, f, acceptabilityFilters...) } // withPreferred is like withAny, but will prioritize recently used nonce // providers. -func (m *multiRPCClient) withPreferred(f func(*provider) error, acceptabilityFilters ...acceptabilityFilter) error { - return m.withOne(m.nonceProviderList(), f, acceptabilityFilters...) +func (m *multiRPCClient) withPreferred(ctx context.Context, f func(context.Context, *provider) error, acceptabilityFilters ...acceptabilityFilter) error { + return m.withOne(ctx, m.nonceProviderList(), f, acceptabilityFilters...) } // nonceProviderList returns the randomized provider list, but with any recent @@ -872,11 +886,13 @@ func (m *multiRPCClient) nextNonce(ctx context.Context) (nonce uint64, err error checkDelay := time.Second * 5 for i := 0; i < checks; i++ { var host string - err = m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + err = m.withPreferred(ctx, func(_ context.Context, p *provider) error { host = p.host + // Pauses are five seconds, do not use the context from + // withPreferred which only allows for ten seconds total. + ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) nonce, err = p.ec.PendingNonceAt(ctx, m.creds.addr) + cancel() return err }) if err != nil { @@ -904,9 +920,7 @@ func (m *multiRPCClient) address() common.Address { } func (m *multiRPCClient) addressBalance(ctx context.Context, addr common.Address) (bal *big.Int, err error) { - return bal, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return bal, m.withAny(ctx, func(ctx context.Context, p *provider) error { bal, err = p.ec.BalanceAt(ctx, addr, nil /* latest */) return err }) @@ -936,14 +950,14 @@ func (m *multiRPCClient) bestHeader(ctx context.Context) (hdr *types.Header, err return bestHeader, nil } - return hdr, m.withAny(func(p *provider) error { + return hdr, m.withAny(ctx, func(ctx context.Context, p *provider) error { hdr, err = p.bestHeader(ctx, m.log) return err }, allRPCErrorsAreFails) } func (m *multiRPCClient) headerByHash(ctx context.Context, h common.Hash) (hdr *types.Header, err error) { - return hdr, m.withAny(func(p *provider) error { + return hdr, m.withAny(ctx, func(ctx context.Context, p *provider) error { hdr, err = p.headerByHash(ctx, h) return err }) @@ -990,9 +1004,7 @@ func (m *multiRPCClient) shutdown() { func (m *multiRPCClient) sendSignedTransaction(ctx context.Context, tx *types.Transaction) error { var lastProvider *provider - if err := m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + if err := m.withPreferred(ctx, func(ctx context.Context, p *provider) error { lastProvider = p m.log.Tracef("Sending signed tx via %q", p.host) return p.ec.SendTransaction(ctx, tx) @@ -1031,14 +1043,16 @@ func (m *multiRPCClient) signData(data []byte) (sig, pubKey []byte, err error) { return signData(m.creds, data) } -// syncProgress: We're going to lie and just always say we're synced if we -// can get a header. -func (m *multiRPCClient) syncProgress(ctx context.Context) (prog *ethereum.SyncProgress, err error) { - return prog, m.withAny(func(p *provider) error { +// syncProgress: Current and Highest blocks are not very useful for the caller, +// but the best header's time in seconds can be used to determine if the +// provider is out of sync. +func (m *multiRPCClient) syncProgress(ctx context.Context) (prog *ethereum.SyncProgress, bestHeaderUNIXTime uint64, err error) { + return prog, bestHeaderUNIXTime, m.withAny(ctx, func(ctx context.Context, p *provider) error { tip, err := p.bestHeader(ctx, m.log) if err != nil { return err } + bestHeaderUNIXTime = tip.Time prog = ðereum.SyncProgress{ CurrentBlock: tip.Number.Uint64(), @@ -1051,14 +1065,16 @@ func (m *multiRPCClient) syncProgress(ctx context.Context) (prog *ethereum.SyncP func (m *multiRPCClient) transactionConfirmations(ctx context.Context, txHash common.Hash) (confs uint32, err error) { var r *types.Receipt var tip *types.Header - if err := m.withPreferred(func(p *provider) error { + if err := m.withPreferred(ctx, func(_ context.Context, p *provider) error { ctxt, cancel := context.WithTimeout(ctx, defaultRequestTimeout) r, err = p.ec.TransactionReceipt(ctxt, txHash) cancel() if err != nil { return err } - tip, err = p.bestHeader(ctx, m.log) + ctxt, cancel = context.WithTimeout(ctx, defaultRequestTimeout) + tip, err = p.bestHeader(ctxt, m.log) + cancel() return err }); err != nil { if isNotFoundError(err) { @@ -1110,8 +1126,10 @@ func (m *multiRPCClient) txOpts(ctx context.Context, val, maxGas uint64, maxFeeR } func (m *multiRPCClient) currentFees(ctx context.Context) (baseFees, tipCap *big.Int, err error) { - return baseFees, tipCap, m.withAny(func(p *provider) error { - hdr, err := p.bestHeader(ctx, m.log) + return baseFees, tipCap, m.withAny(ctx, func(_ context.Context, p *provider) error { + ctxt, cancel := context.WithTimeout(ctx, defaultRequestTimeout) + hdr, err := p.bestHeader(ctxt, m.log) + cancel() if err != nil { return err } @@ -1122,7 +1140,9 @@ func (m *multiRPCClient) currentFees(ctx context.Context) (baseFees, tipCap *big baseFees.Set(minGasPrice) } - tipCap = p.suggestTipCap(ctx, m.log) + ctxt, cancel = context.WithTimeout(ctx, defaultRequestTimeout) + tipCap = p.suggestTipCap(ctxt, m.log) + cancel() return nil }) @@ -1137,70 +1157,56 @@ func (m *multiRPCClient) unlock(pw string) error { var _ bind.ContractBackend = (*multiRPCClient)(nil) func (m *multiRPCClient) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (code []byte, err error) { - return code, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return code, m.withAny(ctx, func(ctx context.Context, p *provider) error { code, err = p.ec.CodeAt(ctx, contract, blockNumber) return err }) } func (m *multiRPCClient) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) (res []byte, err error) { - return res, m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return res, m.withPreferred(ctx, func(ctx context.Context, p *provider) error { res, err = p.ec.CallContract(ctx, call, blockNumber) return err }) } func (m *multiRPCClient) HeaderByNumber(ctx context.Context, number *big.Int) (hdr *types.Header, err error) { - return hdr, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return hdr, m.withAny(ctx, func(ctx context.Context, p *provider) error { hdr, err = p.ec.HeaderByNumber(ctx, number) return err }) } func (m *multiRPCClient) PendingCodeAt(ctx context.Context, account common.Address) (code []byte, err error) { - return code, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return code, m.withAny(ctx, func(ctx context.Context, p *provider) error { code, err = p.ec.PendingCodeAt(ctx, account) return err }) } func (m *multiRPCClient) PendingNonceAt(ctx context.Context, account common.Address) (nonce uint64, err error) { - return nonce, m.withPreferred(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return nonce, m.withPreferred(ctx, func(ctx context.Context, p *provider) error { nonce, err = p.ec.PendingNonceAt(ctx, account) return err }) } func (m *multiRPCClient) SuggestGasPrice(ctx context.Context) (price *big.Int, err error) { - return price, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return price, m.withAny(ctx, func(ctx context.Context, p *provider) error { price, err = p.ec.SuggestGasPrice(ctx) return err }) } func (m *multiRPCClient) SuggestGasTipCap(ctx context.Context) (tipCap *big.Int, err error) { - return tipCap, m.withAny(func(p *provider) error { + return tipCap, m.withAny(ctx, func(ctx context.Context, p *provider) error { tipCap = p.suggestTipCap(ctx, m.log) return nil }) } func (m *multiRPCClient) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) { - return gas, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return gas, m.withAny(ctx, func(ctx context.Context, p *provider) error { gas, err = p.ec.EstimateGas(ctx, call) return err }, func(err error) (discard, propagate, fail bool) { @@ -1214,18 +1220,14 @@ func (m *multiRPCClient) SendTransaction(ctx context.Context, tx *types.Transact } func (m *multiRPCClient) FilterLogs(ctx context.Context, query ethereum.FilterQuery) (logs []types.Log, err error) { - return logs, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return logs, m.withAny(ctx, func(ctx context.Context, p *provider) error { logs, err = p.ec.FilterLogs(ctx, query) return err }) } func (m *multiRPCClient) SubscribeFilterLogs(ctx context.Context, query ethereum.FilterQuery, ch chan<- types.Log) (sub ethereum.Subscription, err error) { - return sub, m.withAny(func(p *provider) error { - ctx, cancel := context.WithTimeout(ctx, defaultRequestTimeout) - defer cancel() + return sub, m.withAny(ctx, func(ctx context.Context, p *provider) error { sub, err = p.ec.SubscribeFilterLogs(ctx, query, ch) return err }) @@ -1242,10 +1244,11 @@ const ( providerInfura = "infura.io" providerRivetCloud = "rivet.cloud" providerAlchemy = "alchemy.com" + providerAnkr = "ankr.com" + providerBlast = "blastapi.io" // Non-compliant providers providerCloudflareETH = "cloudflare-eth.com" // "SuggestGasTipCap" error: Method not found - providerAnkr = "ankr.com" // "SyncProgress" error: the method eth_syncing does not exist/is not available ) var compliantProviders = map[string]struct{}{ @@ -1257,11 +1260,12 @@ var compliantProviders = map[string]struct{}{ providerInfura: {}, providerRivetCloud: {}, providerAlchemy: {}, + providerAnkr: {}, + providerBlast: {}, } var nonCompliantProviders = map[string]struct{}{ providerCloudflareETH: {}, - providerAnkr: {}, } func providerIsCompliant(addr string) (known, compliant bool) { @@ -1275,12 +1279,12 @@ func providerIsCompliant(addr string) (known, compliant bool) { type rpcTest struct { name string - f func(*provider) error + f func(context.Context, *provider) error } // newCompatibilityTests returns a list of RPC tests to run to determine API // compatibility. -func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slog.Logger) []*rpcTest { +func newCompatibilityTests(cb bind.ContractBackend, log slog.Logger) []*rpcTest { var ( // Vitalik's address from https://twitter.com/VitalikButerin/status/1050126908589887488 mainnetAddr = common.HexToAddress("0xab5801a7d398351b8be11c439e05c5b3259aec9b") @@ -1294,35 +1298,35 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo return []*rpcTest{ { name: "HeaderByNumber", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { _, err := p.ec.HeaderByNumber(ctx, nil /* latest */) return err }, }, { name: "HeaderByHash", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { _, err := p.ec.HeaderByHash(ctx, mainnetBlockHash) return err }, }, { name: "TransactionReceipt", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { _, err := p.ec.TransactionReceipt(ctx, mainnetTxHash) return err }, }, { name: "PendingNonceAt", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { _, err := p.ec.PendingNonceAt(ctx, mainnetAddr) return err }, }, { name: "SuggestGasTipCap", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { tipCap, err := p.ec.SuggestGasTipCap(ctx) if err != nil { return err @@ -1333,7 +1337,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "BalanceAt", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { bal, err := p.ec.BalanceAt(ctx, mainnetAddr, nil) if err != nil { return err @@ -1344,7 +1348,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "CodeAt", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { code, err := p.ec.CodeAt(ctx, mainnetUSDC, nil) if err != nil { return err @@ -1355,7 +1359,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "CallContract(balanceOf)", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { caller, err := erc20.NewIERC20(mainnetUSDC, cb) if err != nil { return err @@ -1376,7 +1380,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "ChainID", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { chainID, err := p.ec.ChainID(ctx) if err != nil { return err @@ -1387,7 +1391,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "PendingNonceAt", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { n, err := p.ec.PendingNonceAt(ctx, mainnetAddr) if err != nil { return err @@ -1398,7 +1402,7 @@ func newCompatibilityTests(ctx context.Context, cb bind.ContractBackend, log slo }, { name: "getRPCTransaction", - f: func(p *provider) error { + f: func(ctx context.Context, p *provider) error { rpcTx, err := getRPCTransaction(ctx, p, mainnetTxHash) if err != nil { return err @@ -1449,8 +1453,8 @@ func checkProvidersCompliance(ctx context.Context, walletDir string, providers [ if known, _ := providerIsCompliant(p.host); !known { // Need to run API tests on this endpoint. - for _, t := range newCompatibilityTests(ctx, p.ec, nil /* logger is for testing only */) { - if err := t.f(p); err != nil { + for _, t := range newCompatibilityTests(p.ec, nil /* logger is for testing only */) { + if err := t.f(ctx, p); err != nil { log.Errorf("RPC Provider @ %q has a non-compliant API: %v", err) return fmt.Errorf("RPC Provider @ %q has a non-compliant API", p.host) } diff --git a/client/asset/eth/multirpc_live_test.go b/client/asset/eth/multirpc_live_test.go index 49ef056fbd..14e29cb2a8 100644 --- a/client/asset/eth/multirpc_live_test.go +++ b/client/asset/eth/multirpc_live_test.go @@ -102,6 +102,15 @@ func testEndpoint(endpoints []string, syncBlocks uint64, tFunc func(context.Cont return nil } +func TestMain(m *testing.M) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + // Tests will fail if the best block header is too old. + time.Sleep(time.Second) + mine(ctx) + time.Sleep(time.Second) +} + func TestHTTP(t *testing.T) { if err := testEndpoint([]string{"http://localhost:" + deltaHTTPPort}, 2, nil); err != nil { t.Fatal(err) @@ -272,9 +281,9 @@ func TestRPC(t *testing.T) { t.Fatalf("connect error: %v", err) } - for _, tt := range newCompatibilityTests(ctx, cl, cl.log) { + for _, tt := range newCompatibilityTests(cl, cl.log) { tStart := time.Now() - if err := cl.withAny(tt.f); err != nil { + if err := cl.withAny(ctx, tt.f); err != nil { t.Fatalf("%q: %v", tt.name, err) } fmt.Printf("### %q: %s \n", tt.name, time.Since(tStart)) @@ -304,9 +313,9 @@ func TestFreeServers(t *testing.T) { if err := cl.connect(ctx); err != nil { return fmt.Errorf("connect error: %v", err) } - return cl.withAny(func(p *provider) error { - for _, tt := range newCompatibilityTests(ctx, cl, cl.log) { - if err := tt.f(p); err != nil { + return cl.withAny(ctx, func(ctx context.Context, p *provider) error { + for _, tt := range newCompatibilityTests(cl, cl.log) { + if err := tt.f(ctx, p); err != nil { return fmt.Errorf("%q error: %v", tt.name, err) } fmt.Printf("#### %q passed %q \n", endpoint, tt.name) diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index 999b3cf88e..49e18e51f7 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -248,10 +248,15 @@ func (n *nodeClient) sendTransaction(ctx context.Context, txOpts *bind.TransactO return tx, n.leth.ApiBackend.SendTx(ctx, tx) } -// syncProgress return the current sync progress. Returns no error and nil when not syncing. -func (n *nodeClient) syncProgress(_ context.Context) (*ethereum.SyncProgress, error) { +// syncProgress return the current sync progress and the best block's header +// time in seconds. Returns no error and nil when not syncing. +func (n *nodeClient) syncProgress(ctx context.Context) (*ethereum.SyncProgress, uint64, error) { + hdr, err := n.bestHeader(ctx) + if err != nil { + return nil, 0, err + } p := n.leth.ApiBackend.SyncProgress() - return &p, nil + return &p, hdr.Time, nil } // signData uses the private key of the address to sign a piece of data. diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index e14b698ef6..17d228e56d 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -378,6 +378,18 @@ func runSimnet(m *testing.M) (int, error) { ethSwapContractAddr = dexeth.ContractAddresses[0][dex.Simnet] + // Tests will fail if the best block header is too old. + cmd := exec.CommandContext(ctx, "./mine-alpha", "2") + homeDir, err := os.UserHomeDir() + if err != nil { + return 1, err + } + harnessCtlDir := filepath.Join(homeDir, "dextest", "eth", "harness-ctl") + cmd.Dir = harnessCtlDir + if _, err = cmd.CombinedOutput(); err != nil { + return 1, fmt.Errorf("unexpected error while waiting to mine some blocks: %v", err) + } + initiatorRPC, participantRPC := rpcEndpoints(dex.Simnet) err = setupWallet(simnetWalletDir, simnetWalletSeed, "localhost:30355", initiatorRPC, dex.Simnet) @@ -439,8 +451,6 @@ func runSimnet(m *testing.M) (int, error) { } // Fund the wallets. - homeDir, _ := os.UserHomeDir() - harnessCtlDir := filepath.Join(homeDir, "dextest", "eth", "harness-ctl") send := func(exe, addr, amt string) error { cmd := exec.CommandContext(ctx, exe, addr, amt) cmd.Dir = harnessCtlDir @@ -464,7 +474,7 @@ func runSimnet(m *testing.M) (int, error) { } } - cmd := exec.CommandContext(ctx, "./mine-alpha", "1") + cmd = exec.CommandContext(ctx, "./mine-alpha", "1") cmd.Dir = harnessCtlDir if err := cmd.Run(); err != nil { return 1, fmt.Errorf("error mining block after funding wallets") @@ -738,22 +748,13 @@ func syncClient(cl ethFetcher) error { if err := ctx.Err(); err != nil { return err } - prog, err := cl.syncProgress(ctx) + prog, bestHeaderUNIXTime, err := cl.syncProgress(ctx) if err != nil { return err } if isTestnet { - if prog.HighestBlock == 0 { - bh, err := cl.bestHeader(ctx) - if err != nil { - return err - } - // Time in the header is in seconds. - timeDiff := time.Now().Unix() - int64(bh.Time) - if timeDiff < dexeth.MaxBlockInterval { - return nil - } - } else if prog.CurrentBlock >= prog.HighestBlock { + timeDiff := time.Now().Unix() - int64(bestHeaderUNIXTime) + if timeDiff < dexeth.MaxBlockInterval { return nil } } else { @@ -1112,7 +1113,7 @@ func testSwap(t *testing.T, assetID uint32) { } func testSyncProgress(t *testing.T) { - p, err := ethClient.syncProgress(ctx) + p, _, err := ethClient.syncProgress(ctx) if err != nil { t.Fatal(err) } @@ -2034,7 +2035,10 @@ func testRefund(t *testing.T, assetID uint32) { t.Fatalf("%s: pre-redeem mining error: %v", test.name, err) } - txOpts, _ = participantEthClient.txOpts(ctx, 0, gases.RedeemN(1), nil, nil) + txOpts, err = participantEthClient.txOpts(ctx, 0, gases.RedeemN(1), nil, nil) + if err != nil { + t.Fatalf("%s: txOpts error: %v", test.name, err) + } _, err := pc.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash)}) if err != nil { t.Fatalf("%s: redeem error: %v", test.name, err)