diff --git a/.gitignore b/.gitignore index 2a3e44c999..eac438d24e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ py-ext/dist/ node_modules sdk/typescript/dist +.claude/ diff --git a/BLOCK_TIME_CONFIG.md b/BLOCK_TIME_CONFIG.md new file mode 100644 index 0000000000..f627c54726 --- /dev/null +++ b/BLOCK_TIME_CONFIG.md @@ -0,0 +1,80 @@ +# Block Time Configuration + +This document describes the block time configuration system introduced to fix issue #3974 where timestamps on Optimism (and other chains) were incorrect due to hardcoded Ethereum mainnet block times. + +## Problem + +Previously, TrueBlocks used hardcoded values (13 or 13.3 seconds) for block time calculations across all chains. This caused incorrect timestamp-to-block conversions for chains with different block times like Optimism (2 seconds), Polygon (2 seconds), etc. + +## Solution + +### 1. Configuration Structure + +Added a `blockTime` field to the `ChainGroup` structure in `pkg/configtypes/chain_group.go`: + +```go +type ChainGroup struct { + // ... existing fields ... + BlockTime float64 `json:"blockTime" toml:"blockTime,omitempty"` + // ... rest of fields ... +} +``` + +### 2. Block Time Helper + +Created `pkg/config/blocktime.go` with: +- Default block times for known chains +- `GetBlockTime(chain string)` function that: + - First checks configuration file + - Falls back to known defaults + - Uses Ethereum mainnet (13s) as ultimate fallback + +### 3. Updated Calculations + +Updated hardcoded values in: +- `pkg/tslib/tsdb.go`: Line 100 +- `pkg/identifiers/resolve.go`: Line 201 + +Both now use `config.GetBlockTime(chain)` instead of hardcoded values. + +### 4. Configuration + +Add block times to your `trueBlocks.toml`: + +```toml +[chains.mainnet] +blockTime = 13.0 + +[chains.optimism] +blockTime = 2.0 + +[chains.polygon] +blockTime = 2.0 +``` + +## Default Block Times + +| Chain | Block Time (seconds) | +|-------|---------------------| +| Ethereum Mainnet | 13.0 | +| Optimism | 2.0 | +| Polygon | 2.0 | +| BSC | 3.0 | +| Gnosis | 5.0 | +| Arbitrum | 0.25 | +| Base | 2.0 | +| Avalanche | 2.0 | +| zkSync Era | 1.0 | + +See `pkg/config/blocktime.go` for the complete list. + +## Testing + +Run tests with: +```bash +go test ./pkg/config/blocktime_test.go +``` + +## Migration + +No action required for existing installations. The system will use defaults if `blockTime` is not configured. \ No newline at end of file diff --git a/src/apps/chifra/pkg/config/blocktime.go b/src/apps/chifra/pkg/config/blocktime.go new file mode 100644 index 0000000000..4043e0b62a --- /dev/null +++ b/src/apps/chifra/pkg/config/blocktime.go @@ -0,0 +1,58 @@ +// Copyright 2025 The TrueBlocks Authors. All rights reserved. +// Use of this source code is governed by a license that can +// be found in the LICENSE file. + +package config + +import ( + "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger" +) + +// Default block times for known chains (in seconds) +var defaultBlockTimes = map[string]float64{ + "mainnet": 13.0, // Ethereum mainnet + "optimism": 2.0, // Optimism L2 + "polygon": 2.0, // Polygon PoS + "bsc": 3.0, // BNB Smart Chain + "gnosis": 5.0, // Gnosis Chain (formerly xDai) + "arbitrum": 0.25, // Arbitrum One (very fast, ~250ms) + "base": 2.0, // Base L2 + "sepolia": 13.0, // Sepolia testnet + "goerli": 13.0, // Goerli testnet + "holesky": 12.0, // Holesky testnet + "avalanche": 2.0, // Avalanche C-Chain + "fantom": 1.0, // Fantom Opera + "celo": 5.0, // Celo + "aurora": 1.0, // Aurora (NEAR EVM) + "moonbeam": 12.0, // Moonbeam + "moonriver": 12.0, // Moonriver + "cronos": 6.0, // Cronos + "evmos": 6.0, // Evmos + "kava": 6.0, // Kava EVM + "metis": 4.0, // Metis + "boba": 0.5, // Boba Network + "zksync": 1.0, // zkSync Era + "linea": 12.0, // Linea + "scroll": 3.0, // Scroll + "manta": 2.0, // Manta Pacific +} + +// DefaultBlockTime is the fallback block time when chain-specific config is not available +const DefaultBlockTime = 13.0 // Ethereum mainnet as default + +// GetBlockTime returns the block time for a given chain in seconds +func GetBlockTime(chain string) float64 { + // First, check if configured in config file + if chainConfig := GetChain(chain); chainConfig.BlockTime > 0 { + return chainConfig.BlockTime + } + + // Fall back to known defaults + if blockTime, ok := defaultBlockTimes[chain]; ok { + return blockTime + } + + // Ultimate fallback with warning + logger.Warn("No block time configured for chain, using default", "chain", chain, "default", DefaultBlockTime) + return DefaultBlockTime +} diff --git a/src/apps/chifra/pkg/config/blocktime_test.go b/src/apps/chifra/pkg/config/blocktime_test.go new file mode 100644 index 0000000000..205d6af57c --- /dev/null +++ b/src/apps/chifra/pkg/config/blocktime_test.go @@ -0,0 +1,102 @@ +// Copyright 2025 The TrueBlocks Authors. All rights reserved. +// Use of this source code is governed by a license that can +// be found in the LICENSE file. + +package config + +import ( + "testing" +) + +func TestGetBlockTime(t *testing.T) { + tests := []struct { + name string + chain string + expected float64 + }{ + // Known chains with default block times + { + name: "Ethereum mainnet", + chain: "mainnet", + expected: 13.0, + }, + { + name: "Optimism", + chain: "optimism", + expected: 2.0, + }, + { + name: "Polygon", + chain: "polygon", + expected: 2.0, + }, + { + name: "BSC", + chain: "bsc", + expected: 3.0, + }, + { + name: "Gnosis", + chain: "gnosis", + expected: 5.0, + }, + { + name: "Arbitrum", + chain: "arbitrum", + expected: 0.25, + }, + { + name: "Unknown chain falls back to default", + chain: "unknown_chain", + expected: DefaultBlockTime, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetBlockTime(tt.chain) + if result != tt.expected { + t.Errorf("GetBlockTime(%s) = %f, expected %f", tt.chain, result, tt.expected) + } + }) + } +} + +func TestBlockTimeCalculations(t *testing.T) { + // Test the calculations that were previously hardcoded + tests := []struct { + name string + chain string + secondsDiff float64 + expectedBlks uint32 + }{ + { + name: "Ethereum 1 hour", + chain: "mainnet", + secondsDiff: 3600, + expectedBlks: 276, // 3600 / 13.0 = 276.92 (truncated to 276) + }, + { + name: "Optimism 1 hour", + chain: "optimism", + secondsDiff: 3600, + expectedBlks: 1800, // 3600 / 2.0 = 1800 + }, + { + name: "Arbitrum 1 minute", + chain: "arbitrum", + secondsDiff: 60, + expectedBlks: 240, // 60 / 0.25 = 240 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + blockTime := GetBlockTime(tt.chain) + result := uint32(tt.secondsDiff / blockTime) + if result != tt.expectedBlks { + t.Errorf("Block calculation for %s: got %d blocks, expected %d", tt.chain, result, tt.expectedBlks) + } + }) + } +} \ No newline at end of file diff --git a/src/apps/chifra/pkg/configtypes/chain_group.go b/src/apps/chifra/pkg/configtypes/chain_group.go index 66adaf8cec..910fc4c1e4 100644 --- a/src/apps/chifra/pkg/configtypes/chain_group.go +++ b/src/apps/chifra/pkg/configtypes/chain_group.go @@ -12,6 +12,7 @@ type ChainGroup struct { RpcProviderOld string `json:"rpcProvider,omitempty" toml:"rpcProvider,omitempty"` // deprecated RpcProviders []string `json:"rpcProviders" toml:"rpcProviders,omitempty"` Symbol string `json:"symbol" toml:"symbol"` + BlockTime float64 `json:"blockTime" toml:"blockTime,omitempty"` Scrape ScrapeSettings `json:"scrape" toml:"scrape"` } diff --git a/src/apps/chifra/pkg/identifiers/resolve.go b/src/apps/chifra/pkg/identifiers/resolve.go index 742da0b022..a8dd6f2225 100644 --- a/src/apps/chifra/pkg/identifiers/resolve.go +++ b/src/apps/chifra/pkg/identifiers/resolve.go @@ -9,6 +9,7 @@ import ( "fmt" "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base" + "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/ranges" "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/rpc" "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/tslib" @@ -197,7 +198,8 @@ func (p *Point) resolvePoint(chain string) base.Blknum { latest := conn.GetLatestBlockNumber() tsFuture := conn.GetBlockTimestamp(latest) secs := base.Blknum(tsFuture - base.Timestamp(p.Number)) - blks := (secs / 13) + blockTime := config.GetBlockTime(chain) + blks := base.Blknum(float64(secs) / blockTime) bn = latest + blks } } else { diff --git a/src/apps/chifra/pkg/tslib/tsdb.go b/src/apps/chifra/pkg/tslib/tsdb.go index 21283d977e..3bafdefb5c 100644 --- a/src/apps/chifra/pkg/tslib/tsdb.go +++ b/src/apps/chifra/pkg/tslib/tsdb.go @@ -97,7 +97,8 @@ func FromTs(chain string, ts base.Timestamp) (*TimestampRecord, error) { if ts > base.Timestamp(perChainTimestamps[chain].memory[cnt-1].Ts) { last := perChainTimestamps[chain].memory[cnt-1] secs := ts - base.Timestamp(last.Ts) - blks := uint32(float64(secs) / 13.3) + blockTime := config.GetBlockTime(chain) + blks := uint32(float64(secs) / blockTime) last.Bn = last.Bn + blks last.Ts = uint32(ts) return &last, ErrInTheFuture diff --git a/src/other/install/trueBlocks.toml b/src/other/install/trueBlocks.toml index 46a3cbb42f..951c8d23f0 100644 --- a/src/other/install/trueBlocks.toml +++ b/src/other/install/trueBlocks.toml @@ -36,6 +36,7 @@ remoteExplorer = 'https://gnosisscan.io/' rpcProviders = ['https://gnosischain-rpc.gateway.pokt.network'] symbol = 'XDAI' + blockTime = 5.0 [chains.gnosis.scrape] appsPerChunk = 2000000 snapToGrid = 250000 @@ -51,6 +52,7 @@ remoteExplorer = 'https://etherscan.io/' rpcProviders = ['http://localhost:8545'] symbol = 'ETH' + blockTime = 13.0 [chains.mainnet.scrape] appsPerChunk = 2000000 snapToGrid = 100000 @@ -66,6 +68,7 @@ remoteExplorer = 'https://optimistic.etherscan.io/' rpcProviders = ['https://mainnet.optimism.io'] symbol = 'OPT' + blockTime = 2.0 [chains.optimism.scrape] appsPerChunk = 2000000 snapToGrid = 250000 @@ -81,6 +84,7 @@ remoteExplorer = 'https://mumbai.polygonscan.com/' rpcProviders = ['https://rpc-mumbai.maticvigil.com'] symbol = 'MATIC' + blockTime = 2.0 [chains.polygon.scrape] appsPerChunk = 2000000 snapToGrid = 250000 @@ -96,6 +100,7 @@ remoteExplorer = 'https://sepolia.otterscan.io/' rpcProviders = ['http://localhost:8548'] symbol = 'ETH' + blockTime = 13.0 [chains.sepolia.scrape] appsPerChunk = 2000000 snapToGrid = 250000