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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ py-ext/dist/

node_modules
sdk/typescript/dist
.claude/
80 changes: 80 additions & 0 deletions BLOCK_TIME_CONFIG.md
Original file line number Diff line number Diff line change
@@ -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.
58 changes: 58 additions & 0 deletions src/apps/chifra/pkg/config/blocktime.go
Original file line number Diff line number Diff line change
@@ -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
}
102 changes: 102 additions & 0 deletions src/apps/chifra/pkg/config/blocktime_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions src/apps/chifra/pkg/configtypes/chain_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
4 changes: 3 additions & 1 deletion src/apps/chifra/pkg/identifiers/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion src/apps/chifra/pkg/tslib/tsdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/other/install/trueBlocks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -51,6 +52,7 @@
remoteExplorer = 'https://etherscan.io/'
rpcProviders = ['http://localhost:8545']
symbol = 'ETH'
blockTime = 13.0
[chains.mainnet.scrape]
appsPerChunk = 2000000
snapToGrid = 100000
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down