Skip to content

Commit 35b9af3

Browse files
committed
feat(chainsync): add EpochNumber to ChainSyncStatus
Closes #187 Signed-off-by: Chris Gianelloni <[email protected]>
1 parent f4fecb1 commit 35b9af3

File tree

3 files changed

+170
-43
lines changed

3 files changed

+170
-43
lines changed

input/chainsync/chainsync.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,43 @@ import (
4040
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
4141
)
4242

43+
// getNetwork returns the configured Cardano network name, or "mainnet" if not set.
44+
func getNetwork() string {
45+
conf := config.GetConfig()
46+
if conf.Plugin != nil {
47+
if input, ok := conf.Plugin["input"]; ok {
48+
if chainsync, ok := input["chainsync"]; ok {
49+
if net, ok := chainsync["network"]; ok {
50+
if s, ok := net.(string); ok {
51+
return s
52+
}
53+
}
54+
}
55+
}
56+
}
57+
return "mainnet"
58+
}
59+
60+
// EpochFromSlot returns the epoch number for a given slot, using Cardano era rules.
61+
func EpochFromSlot(slot uint64) uint64 {
62+
cfg := config.GetConfig()
63+
byron := cfg.ByronGenesis
64+
shelley := cfg.ShelleyGenesis
65+
66+
if slot <= byron.EndSlot {
67+
if byron.EpochLength == 0 {
68+
return 0 // avoid div by zero
69+
}
70+
return slot / byron.EpochLength
71+
}
72+
shelleyStartEpoch := byron.Epochs
73+
shelleyStartSlot := byron.EndSlot + 1
74+
if shelley.EpochLength == 0 {
75+
return shelleyStartEpoch // avoid div by zero
76+
}
77+
return shelleyStartEpoch + (slot-shelleyStartSlot)/shelley.EpochLength
78+
}
79+
4380
const (
4481
// Size of cache for recent chainsync cursors
4582
cursorCacheSize = 20
@@ -84,6 +121,7 @@ type ChainSyncStatus struct {
84121
TipBlockHash string
85122
SlotNumber uint64
86123
BlockNumber uint64
124+
EpochNumber uint64
87125
TipSlotNumber uint64
88126
TipReached bool
89127
}
@@ -522,6 +560,7 @@ func (c *ChainSync) updateStatus(
522560
c.status.SlotNumber = slotNumber
523561
c.status.BlockNumber = blockNumber
524562
c.status.BlockHash = blockHash
563+
c.status.EpochNumber = EpochFromSlot(slotNumber)
525564
c.status.TipSlotNumber = tipSlotNumber
526565
c.status.TipBlockHash = tipBlockHash
527566
if c.statusUpdateFunc != nil {

input/chainsync/chainsync_test.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
// Copyright 2025 Blink Labs Software
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
114
package chainsync
215

316
import (
@@ -63,16 +76,14 @@ func TestHandleRollBackward(t *testing.T) {
6376
t.Fatal("Expected event was not sent to eventChan")
6477
}
6578

66-
// Verify that the status was updated correctly
67-
assert.Equal(t, uint64(12345), c.status.SlotNumber)
68-
assert.Equal(
69-
t,
70-
uint64(0),
71-
c.status.BlockNumber,
72-
) // BlockNumber should be 0 after rollback
73-
assert.Equal(t, "0102030405", c.status.BlockHash)
74-
assert.Equal(t, uint64(67890), c.status.TipSlotNumber)
75-
assert.Equal(t, "060708090a", c.status.TipBlockHash)
79+
// Verify that the status was updated correctly
80+
assert.Equal(t, uint64(12345), c.status.SlotNumber)
81+
assert.Equal(t, uint64(0), c.status.BlockNumber) // BlockNumber should be 0 after rollback
82+
assert.Equal(t, "0102030405", c.status.BlockHash)
83+
assert.Equal(t, uint64(67890), c.status.TipSlotNumber)
84+
assert.Equal(t, "060708090a", c.status.TipBlockHash)
85+
// New: Check EpochNumber
86+
assert.Equal(t, uint64(0), c.status.EpochNumber) // 12345/432000 = 0
7687
}
7788

7889
func TestGetKupoClient(t *testing.T) {

internal/config/config.go

Lines changed: 110 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,68 @@ import (
2424
"gopkg.in/yaml.v2"
2525
)
2626

27+
// ByronGenesisConfig holds Byron era genesis parameters
28+
// ShelleyGenesisConfig holds Shelley era genesis parameters
29+
// ShelleyTransEpoch holds transition slot/epoch info
30+
31+
// ByronGenesisConfig holds Byron era genesis parameters
32+
type ByronGenesisConfig struct {
33+
EpochLength uint64 `yaml:"epoch_length"`
34+
ByronSlotsPerEpoch uint64 `yaml:"byron_slots_per_epoch"`
35+
EndSlot uint64 `yaml:"end_slot"`
36+
Epochs uint64 `yaml:"epochs"`
37+
}
38+
39+
// ShelleyGenesisConfig holds Shelley era genesis parameters
40+
type ShelleyGenesisConfig struct {
41+
EpochLength uint64 `yaml:"epoch_length"`
42+
}
43+
44+
// ShelleyTransEpoch holds transition slot/epoch info
45+
type ShelleyTransEpoch struct {
46+
FirstShelleySlot uint64 `yaml:"first_shelley_slot"`
47+
FirstShelleyEpoch uint64 `yaml:"first_shelley_epoch"`
48+
}
49+
50+
// populateByronGenesis sets defaults and validates ByronGenesisConfig
51+
func (c *Config) populateByronGenesis() error {
52+
cfg := &c.ByronGenesis
53+
if cfg.EpochLength == 0 {
54+
cfg.EpochLength = 21600
55+
}
56+
if cfg.ByronSlotsPerEpoch == 0 {
57+
cfg.ByronSlotsPerEpoch = cfg.EpochLength
58+
}
59+
if cfg.EndSlot == 0 {
60+
cfg.EndSlot = 4492799
61+
}
62+
if cfg.Epochs == 0 {
63+
cfg.Epochs = 208
64+
}
65+
if cfg.EpochLength == 0 {
66+
return fmt.Errorf("ByronGenesisConfig: EpochLength must be nonzero")
67+
}
68+
if cfg.EndSlot == 0 {
69+
return fmt.Errorf("ByronGenesisConfig: EndSlot must be nonzero")
70+
}
71+
if cfg.Epochs == 0 {
72+
return fmt.Errorf("ByronGenesisConfig: Epochs must be nonzero")
73+
}
74+
return nil
75+
}
76+
77+
// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig
78+
func (c *Config) populateShelleyGenesis() error {
79+
cfg := &c.ShelleyGenesis
80+
if cfg.EpochLength == 0 {
81+
cfg.EpochLength = 432000
82+
}
83+
if cfg.EpochLength == 0 {
84+
return fmt.Errorf("ShelleyGenesisConfig: EpochLength must be nonzero")
85+
}
86+
return nil
87+
}
88+
2789
const (
2890
DefaultInputPlugin = "chainsync"
2991
DefaultOutputPlugin = "log"
@@ -38,6 +100,8 @@ type Config struct {
38100
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
39101
Api ApiConfig `yaml:"api"`
40102
Debug DebugConfig `yaml:"debug"`
103+
ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"`
104+
ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"`
41105
Version bool `yaml:"-"`
42106
}
43107

@@ -57,42 +121,55 @@ type DebugConfig struct {
57121

58122
// Singleton config instance with default values
59123
var globalConfig = &Config{
60-
Api: ApiConfig{
61-
ListenAddress: "0.0.0.0",
62-
ListenPort: 8080,
63-
},
64-
Logging: LoggingConfig{
65-
Level: "info",
66-
},
67-
Debug: DebugConfig{
68-
ListenAddress: "localhost",
69-
ListenPort: 0,
70-
},
71-
Input: DefaultInputPlugin,
72-
Output: DefaultOutputPlugin,
73-
KupoUrl: "",
124+
Api: ApiConfig{
125+
ListenAddress: "0.0.0.0",
126+
ListenPort: 8080,
127+
},
128+
Logging: LoggingConfig{
129+
Level: "info",
130+
},
131+
Debug: DebugConfig{
132+
ListenAddress: "localhost",
133+
ListenPort: 0,
134+
},
135+
Input: DefaultInputPlugin,
136+
Output: DefaultOutputPlugin,
137+
KupoUrl: "",
138+
ByronGenesis: ByronGenesisConfig{
139+
EpochLength: 21600,
140+
EndSlot: 4492799,
141+
Epochs: 208,
142+
},
143+
ShelleyGenesis: ShelleyGenesisConfig{
144+
EpochLength: 432000,
145+
},
74146
}
75147

76148
func (c *Config) Load(configFile string) error {
77-
// Load config file as YAML if provided
78-
if configFile != "" {
79-
buf, err := os.ReadFile(configFile)
80-
if err != nil {
81-
return fmt.Errorf("error reading config file: %w", err)
82-
}
83-
err = yaml.Unmarshal(buf, c)
84-
if err != nil {
85-
return fmt.Errorf("error parsing config file: %w", err)
86-
}
87-
}
88-
// Load config values from environment variables
89-
// We use "dummy" as the app name here to (mostly) prevent picking up env
90-
// vars that we hadn't explicitly specified in annotations above
91-
err := envconfig.Process("dummy", c)
92-
if err != nil {
93-
return fmt.Errorf("error processing environment: %w", err)
94-
}
95-
return nil
149+
// Load config file as YAML if provided
150+
if configFile != "" {
151+
buf, err := os.ReadFile(configFile)
152+
if err != nil {
153+
return fmt.Errorf("error reading config file: %w", err)
154+
}
155+
err = yaml.Unmarshal(buf, c)
156+
if err != nil {
157+
return fmt.Errorf("error parsing config file: %w", err)
158+
}
159+
}
160+
// Load config values from environment variables
161+
err := envconfig.Process("dummy", c)
162+
if err != nil {
163+
return fmt.Errorf("error processing environment: %w", err)
164+
}
165+
// Populate Byron and Shelley genesis configs (from nview)
166+
if err := c.populateByronGenesis(); err != nil {
167+
return fmt.Errorf("error populating Byron genesis: %w", err)
168+
}
169+
if err := c.populateShelleyGenesis(); err != nil {
170+
return fmt.Errorf("error populating Shelley genesis: %w", err)
171+
}
172+
return nil
96173
}
97174

98175
func (c *Config) ParseCmdlineArgs(programName string, args []string) error {

0 commit comments

Comments
 (0)