Skip to content

Commit 0aa49be

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

File tree

3 files changed

+127
-16
lines changed

3 files changed

+127
-16
lines changed

input/chainsync/chainsync.go

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

43+
// EpochFromSlot returns the epoch number for a given slot, using Cardano era rules.
44+
func EpochFromSlot(slot uint64) uint64 {
45+
cfg := config.GetConfig()
46+
byron := cfg.ByronGenesis
47+
shelley := cfg.ShelleyGenesis
48+
49+
endSlot := uint64(0)
50+
if byron.EndSlot != nil {
51+
endSlot = *byron.EndSlot
52+
}
53+
epochs := uint64(0)
54+
if byron.Epochs != nil {
55+
epochs = *byron.Epochs
56+
}
57+
if slot <= endSlot {
58+
if byron.EpochLength == 0 {
59+
return 0 // avoid div by zero
60+
}
61+
return slot / byron.EpochLength
62+
}
63+
shelleyStartEpoch := epochs
64+
shelleyStartSlot := endSlot + 1
65+
if shelley.EpochLength == 0 {
66+
return shelleyStartEpoch // avoid div by zero
67+
}
68+
return shelleyStartEpoch + (slot-shelleyStartSlot)/shelley.EpochLength
69+
}
70+
4371
const (
4472
// Size of cache for recent chainsync cursors
4573
cursorCacheSize = 20
@@ -84,6 +112,7 @@ type ChainSyncStatus struct {
84112
TipBlockHash string
85113
SlotNumber uint64
86114
BlockNumber uint64
115+
EpochNumber uint64
87116
TipSlotNumber uint64
88117
TipReached bool
89118
}
@@ -522,6 +551,7 @@ func (c *ChainSync) updateStatus(
522551
c.status.SlotNumber = slotNumber
523552
c.status.BlockNumber = blockNumber
524553
c.status.BlockHash = blockHash
554+
c.status.EpochNumber = EpochFromSlot(slotNumber)
525555
c.status.TipSlotNumber = tipSlotNumber
526556
c.status.TipBlockHash = tipBlockHash
527557
if c.statusUpdateFunc != nil {

input/chainsync/chainsync_test.go

Lines changed: 16 additions & 5 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 (
@@ -65,14 +78,12 @@ func TestHandleRollBackward(t *testing.T) {
6578

6679
// Verify that the status was updated correctly
6780
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
81+
assert.Equal(t, uint64(0), c.status.BlockNumber) // BlockNumber should be 0 after rollback
7382
assert.Equal(t, "0102030405", c.status.BlockHash)
7483
assert.Equal(t, uint64(67890), c.status.TipSlotNumber)
7584
assert.Equal(t, "060708090a", c.status.TipBlockHash)
85+
// New: Check EpochNumber (Byron era: 12345/21600 = 0)
86+
assert.Equal(t, uint64(0), c.status.EpochNumber)
7687
}
7788

7889
func TestGetKupoClient(t *testing.T) {

internal/config/config.go

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package config
1616

1717
import (
18+
"errors"
1819
"flag"
1920
"fmt"
2021
"os"
@@ -24,21 +25,79 @@ import (
2425
"gopkg.in/yaml.v2"
2526
)
2627

28+
// ByronGenesisConfig holds Byron era genesis parameters
29+
type ByronGenesisConfig struct {
30+
EpochLength uint64 `yaml:"epoch_length"`
31+
ByronSlotsPerEpoch uint64 `yaml:"byron_slots_per_epoch"`
32+
EndSlot *uint64 `yaml:"end_slot"`
33+
Epochs *uint64 `yaml:"epochs"`
34+
}
35+
36+
// ShelleyGenesisConfig holds Shelley era genesis parameters
37+
type ShelleyGenesisConfig struct {
38+
EpochLength uint64 `yaml:"epoch_length"`
39+
}
40+
41+
// ShelleyTransEpoch holds transition slot/epoch info
42+
type ShelleyTransEpoch struct {
43+
FirstShelleySlot uint64 `yaml:"first_shelley_slot"`
44+
FirstShelleyEpoch uint64 `yaml:"first_shelley_epoch"`
45+
}
46+
47+
// populateByronGenesis sets defaults and validates ByronGenesisConfig
48+
func (c *Config) populateByronGenesis() error {
49+
cfg := &c.ByronGenesis
50+
if cfg.EpochLength == 0 {
51+
cfg.EpochLength = 21600
52+
}
53+
if cfg.ByronSlotsPerEpoch == 0 {
54+
cfg.ByronSlotsPerEpoch = cfg.EpochLength
55+
}
56+
if cfg.EndSlot == nil {
57+
defaultEndSlot := uint64(4492799)
58+
cfg.EndSlot = &defaultEndSlot
59+
}
60+
if cfg.Epochs == nil {
61+
defaultEpochs := uint64(208)
62+
cfg.Epochs = &defaultEpochs
63+
}
64+
if cfg.EpochLength == 0 {
65+
return errors.New("ByronGenesisConfig: EpochLength must be nonzero")
66+
}
67+
if cfg.EndSlot == nil {
68+
return errors.New("ByronGenesisConfig: EndSlot must be set (can be zero for Byron-less networks)")
69+
}
70+
if cfg.Epochs == nil {
71+
return errors.New("ByronGenesisConfig: Epochs must be set (can be zero for Byron-less networks)")
72+
}
73+
return nil
74+
}
75+
76+
// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig
77+
func (c *Config) populateShelleyGenesis() {
78+
cfg := &c.ShelleyGenesis
79+
if cfg.EpochLength == 0 {
80+
cfg.EpochLength = 432000
81+
}
82+
}
83+
2784
const (
2885
DefaultInputPlugin = "chainsync"
2986
DefaultOutputPlugin = "log"
3087
)
3188

3289
type Config struct {
33-
Plugin map[string]map[string]map[any]any `yaml:"plugins"`
34-
Logging LoggingConfig `yaml:"logging"`
35-
ConfigFile string `yaml:"-"`
36-
Input string `yaml:"input" envconfig:"INPUT"`
37-
Output string `yaml:"output" envconfig:"OUTPUT"`
38-
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
39-
Api ApiConfig `yaml:"api"`
40-
Debug DebugConfig `yaml:"debug"`
41-
Version bool `yaml:"-"`
90+
Plugin map[string]map[string]map[any]any `yaml:"plugins"`
91+
Logging LoggingConfig `yaml:"logging"`
92+
ConfigFile string `yaml:"-"`
93+
Input string `yaml:"input" envconfig:"INPUT"`
94+
Output string `yaml:"output" envconfig:"OUTPUT"`
95+
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
96+
Api ApiConfig `yaml:"api"`
97+
Debug DebugConfig `yaml:"debug"`
98+
ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"`
99+
ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"`
100+
Version bool `yaml:"-"`
42101
}
43102

44103
type ApiConfig struct {
@@ -71,6 +130,14 @@ var globalConfig = &Config{
71130
Input: DefaultInputPlugin,
72131
Output: DefaultOutputPlugin,
73132
KupoUrl: "",
133+
ByronGenesis: ByronGenesisConfig{
134+
EpochLength: 21600,
135+
EndSlot: func() *uint64 { v := uint64(4492799); return &v }(),
136+
Epochs: func() *uint64 { v := uint64(208); return &v }(),
137+
},
138+
ShelleyGenesis: ShelleyGenesisConfig{
139+
EpochLength: 432000,
140+
},
74141
}
75142

76143
func (c *Config) Load(configFile string) error {
@@ -86,12 +153,15 @@ func (c *Config) Load(configFile string) error {
86153
}
87154
}
88155
// 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
91156
err := envconfig.Process("dummy", c)
92157
if err != nil {
93158
return fmt.Errorf("error processing environment: %w", err)
94159
}
160+
// Populate Byron and Shelley genesis configs (from nview)
161+
if err := c.populateByronGenesis(); err != nil {
162+
return fmt.Errorf("error populating Byron genesis: %w", err)
163+
}
164+
c.populateShelleyGenesis()
95165
return nil
96166
}
97167

0 commit comments

Comments
 (0)