Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion bchain/coins/eth/ethrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type Configuration struct {
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"`
Expand Down Expand Up @@ -155,6 +156,11 @@ func NewEthereumRPC(config json.RawMessage, pushHandler func(bchain.Notification
if c.AddressContractsCacheMaxBytes <= 0 {
c.AddressContractsCacheMaxBytes = defaultAddressContractsCacheMaxBytes
}
if c.TraceTimeout != "" {
if _, err := time.ParseDuration(c.TraceTimeout); err != nil {
return nil, errors.Annotatef(err, "invalid trace_timeout")
}
}

s := &EthereumRPC{
BaseChain: &bchain.BaseChain{},
Expand Down Expand Up @@ -998,7 +1004,11 @@ func (b *EthereumRPC) getInternalDataForBlock(ctx context.Context, blockHash str
contracts := make([]bchain.ContractInfo, 0)
if bchain.ProcessInternalTransactions {
var trace []rpcTraceResult
err := b.RPC.CallContext(ctx, &trace, "debug_traceBlockByHash", blockHash, map[string]interface{}{"tracer": "callTracer"}) // Use caller-provided ctx for timeout/cancel.
traceConfig := map[string]interface{}{"tracer": "callTracer"}
if b.ChainConfig.TraceTimeout != "" {
traceConfig["timeout"] = b.ChainConfig.TraceTimeout
}
err := b.RPC.CallContext(ctx, &trace, "debug_traceBlockByHash", blockHash, traceConfig) // Use caller-provided ctx for timeout/cancel.
if err != nil {
glog.Error("debug_traceBlockByHash block ", blockHash, ", error ", err)
return data, contracts, err
Expand Down
105 changes: 105 additions & 0 deletions bchain/coins/eth/ethrpc_trace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package eth

import (
"context"
"encoding/json"
"errors"
"testing"

"github.com/trezor/blockbook/bchain"
)

type mockTraceRPC struct {
method string
args []interface{}
}

func (m *mockTraceRPC) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (bchain.EVMClientSubscription, error) {
return nil, errors.New("not implemented")
}

func (m *mockTraceRPC) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
m.method = method
m.args = append([]interface{}{}, args...)
if out, ok := result.(*[]rpcTraceResult); ok {
*out = []rpcTraceResult{}
}
return nil
}

func (m *mockTraceRPC) Close() {}

func TestNewEthereumRPCRejectsInvalidTraceTimeout(t *testing.T) {
_, err := NewEthereumRPC(json.RawMessage(`{
"coin_name":"Ethereum",
"coin_shortcut":"ETH",
"rpc_timeout":25,
"trace_timeout":"not-a-duration",
"block_addresses_to_keep":600
}`), nil)
if err == nil {
t.Fatal("expected invalid trace_timeout error")
}
}

func TestGetInternalDataForBlockIncludesTraceTimeout(t *testing.T) {
rpcClient := &mockTraceRPC{}
b := &EthereumRPC{
RPC: rpcClient,
ChainConfig: &Configuration{
ProcessInternalTransactions: true,
TraceTimeout: "20s",
},
}
bchain.ProcessInternalTransactions = true
t.Cleanup(func() {
bchain.ProcessInternalTransactions = false
})

_, _, err := b.getInternalDataForBlock(context.Background(), "0xabc", 1, nil)
if err != nil {
t.Fatalf("getInternalDataForBlock() error = %v", err)
}
if rpcClient.method != "debug_traceBlockByHash" {
t.Fatalf("method = %q, want %q", rpcClient.method, "debug_traceBlockByHash")
}
if len(rpcClient.args) != 2 {
t.Fatalf("args len = %d, want 2", len(rpcClient.args))
}
traceConfig, ok := rpcClient.args[1].(map[string]interface{})
if !ok {
t.Fatalf("trace config type = %T, want map[string]interface{}", rpcClient.args[1])
}
if got := traceConfig["tracer"]; got != "callTracer" {
t.Fatalf("tracer = %#v, want %q", got, "callTracer")
}
if got := traceConfig["timeout"]; got != "20s" {
t.Fatalf("timeout = %#v, want %q", got, "20s")
}
}

func TestGetInternalDataForBlockOmitsTraceTimeoutWhenUnset(t *testing.T) {
rpcClient := &mockTraceRPC{}
b := &EthereumRPC{
RPC: rpcClient,
ChainConfig: &Configuration{
ProcessInternalTransactions: true,
},
}
bchain.ProcessInternalTransactions = true
t.Cleanup(func() {
bchain.ProcessInternalTransactions = false
})

_, _, err := b.getInternalDataForBlock(context.Background(), "0xabc", 1, nil)
if err != nil {
t.Fatalf("getInternalDataForBlock() error = %v", err)
}
traceConfig, ok := rpcClient.args[1].(map[string]interface{})
if !ok {
t.Fatalf("trace config type = %T, want map[string]interface{}", rpcClient.args[1])
}
if _, ok := traceConfig["timeout"]; ok {
t.Fatalf("timeout should be omitted when unset, config = %#v", traceConfig)
}
}
13 changes: 7 additions & 6 deletions bchain/config_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ var testEnvMu sync.Mutex
// BlockchainCfg contains fields read from blockbook's blockchaincfg.json after being rendered from templates.
type BlockchainCfg struct {
// more fields can be added later as needed
RpcUrl string `json:"rpc_url"`
RpcUrlWs string `json:"rpc_url_ws"`
RpcUser string `json:"rpc_user"`
RpcPass string `json:"rpc_pass"`
RpcTimeout int `json:"rpc_timeout"`
Parse bool `json:"parse"`
RpcUrl string `json:"rpc_url"`
RpcUrlWs string `json:"rpc_url_ws"`
RpcUser string `json:"rpc_user"`
RpcPass string `json:"rpc_pass"`
RpcTimeout int `json:"rpc_timeout"`
TraceTimeout string `json:"trace_timeout"`
Parse bool `json:"parse"`
}

// LoadBlockchainCfg returns the resolved blockchaincfg.json (env overrides are honored in tests)
Expand Down
9 changes: 6 additions & 3 deletions blockbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import (
"github.com/trezor/blockbook/server"
)

// debounce too close requests for resync
const debounceResyncIndexMs = 1009
// default debounce for too-close requests for resync
const defaultResyncIndexDebounceMs = 1009

// debounce too close requests for resync mempool (ZeroMQ sends message for each tx, when new block there are many transactions)
const debounceResyncMempoolMs = 1009
Expand Down Expand Up @@ -82,6 +82,9 @@ var (
// resync index at least each resyncIndexPeriodMs (could be more often if invoked by message from ZeroMQ)
resyncIndexPeriodMs = flag.Int("resyncindexperiod", 935093, "resync index period in milliseconds")

// debounce for push-triggered index resync requests
resyncIndexDebounceMs = flag.Int("resyncindexdebounce", defaultResyncIndexDebounceMs, "debounce for push-triggered index resync requests in milliseconds")

// resync mempool at least each resyncMempoolPeriodMs (could be more often if invoked by message from ZeroMQ)
resyncMempoolPeriodMs = flag.Int("resyncmempoolperiod", 60017, "resync mempool period in milliseconds")

Expand Down Expand Up @@ -544,7 +547,7 @@ func syncIndexLoop() {
defer close(chanSyncIndexDone)
glog.Info("syncIndexLoop starting")
// resync index about every 15 minutes if there are no chanSyncIndex requests, with debounce 1 second
common.TickAndDebounce(time.Duration(*resyncIndexPeriodMs)*time.Millisecond, debounceResyncIndexMs*time.Millisecond, chanSyncIndex, func() {
common.TickAndDebounce(time.Duration(*resyncIndexPeriodMs)*time.Millisecond, time.Duration(*resyncIndexDebounceMs)*time.Millisecond, chanSyncIndex, func() {
if err := syncWorker.ResyncIndex(onNewBlock, false); err != nil {
if err == db.ErrOperationInterrupted || common.IsInShutdown() {
return
Expand Down
3 changes: 2 additions & 1 deletion configs/coins/arbitrum_archive.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "",
"additional_params": "-workers=16",
"additional_params": "-workers=16 -resyncindexdebounce=1509",
"block_chain": {
"parse": true,
"mempool_workers": 8,
Expand All @@ -58,6 +58,7 @@
"alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/42161/suggestedGasFees\", \"periodSeconds\": 16}",
"mempoolTxTimeoutHours": 48,
"processInternalTransactions": true,
"trace_timeout": "20s",
"queryBackendOnMempoolResync": false,
"fiat_rates": "coingecko",
"fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH",
Expand Down
3 changes: 2 additions & 1 deletion configs/coins/polygon_archive.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"internal_binding_template": ":{{.Ports.BlockbookInternal}}",
"public_binding_template": ":{{.Ports.BlockbookPublic}}",
"explorer_url": "",
"additional_params": "-workers=16",
"additional_params": "-workers=16 -resyncindexdebounce=1509",
"block_chain": {
"parse": true,
"mempool_workers": 8,
Expand All @@ -65,6 +65,7 @@
"alternative_estimate_fee_params": "{\"url\": \"https://gas.api.infura.io/v3/${api_key}/networks/137/suggestedGasFees\", \"periodSeconds\": 8}",
"mempoolTxTimeoutHours": 48,
"processInternalTransactions": true,
"trace_timeout": "20s",
"queryBackendOnMempoolResync": false,
"fiat_rates": "coingecko",
"fiat_rates_vs_currencies": "AED,ARS,AUD,BDT,BHD,BMD,BRL,CAD,CHF,CLP,CNY,CZK,DKK,EUR,GBP,HKD,HUF,IDR,ILS,INR,JPY,KRW,KWD,LKR,MMK,MXN,MYR,NGN,NOK,NZD,PHP,PKR,PLN,RUB,SAR,SEK,SGD,THB,TRY,TWD,UAH,USD,VEF,VND,ZAR,BTC,ETH",
Expand Down
2 changes: 2 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ Good examples of coin configuration are
* `hot_address_min_contracts` – Minimum number of contracts before hotness tracking applies (default **192**).
* `hot_address_min_hits` – Lookups within the current block required to mark an address hot (default **3**, clamped to **10**).
* `hot_address_lru_cache_size` – Max hot addresses kept in the LRU (default **20000**, clamped to **100,000**).
* Ethereum trace configuration (Blockbook, Ethereum-type indexing):
* `trace_timeout` – Optional per-request timeout passed to `debug_traceBlockByHash` as tracer config, formatted as a Go duration string such as `"20s"`.
* Address-contracts cache configuration (Blockbook, Ethereum-type indexing):
* `address_contracts_cache_min_size` – Minimum packed size (bytes) before an addressContracts entry is cached (default **300000**).
* `address_contracts_cache_max_bytes` – Cache size cap in bytes; when exceeded, cached entries are flushed early (default **4000000000**).
Expand Down
25 changes: 25 additions & 0 deletions tests/config_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,28 @@ func TestLoadBlockchainCfgEnvOverride(t *testing.T) {
})
}
}

func TestLoadBlockchainCfgTraceTimeout(t *testing.T) {
tests := []struct {
coinAlias string
want string
}{
{
coinAlias: "polygon_archive",
want: "20s",
},
{
coinAlias: "arbitrum_archive",
want: "20s",
},
}

for _, tt := range tests {
t.Run(tt.coinAlias, func(t *testing.T) {
cfg := bchain.LoadBlockchainCfg(t, tt.coinAlias)
if cfg.TraceTimeout != tt.want {
t.Fatalf("expected trace_timeout %q, got %q", tt.want, cfg.TraceTimeout)
}
})
}
}
Loading