Skip to content

Commit 1f0cc86

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

File tree

5 files changed

+129
-25
lines changed

5 files changed

+129
-25
lines changed

filter/chainsync/chainsync_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ func (m *MockAddress) UnmarshalCBOR(data []byte) error {
9292
// MockOutput is a mock implementation of the TransactionOutput interface
9393
type MockOutput struct {
9494
address ledger.Address
95-
amount uint64
95+
scriptRef common.Script
9696
assets *common.MultiAsset[common.MultiAssetTypeOutput]
9797
datum *common.Datum
98-
scriptRef common.Script
98+
amount uint64
9999
}
100100

101101
func (m MockOutput) Address() ledger.Address {
@@ -207,15 +207,15 @@ func TestChainSync_OutputChan(t *testing.T) {
207207

208208
// Mock certificate implementations
209209
type mockStakeDelegationCert struct {
210-
common.StakeDelegationCertificate
211210
cborData []byte
211+
common.StakeDelegationCertificate
212212
}
213213

214214
func (m *mockStakeDelegationCert) Cbor() []byte { return m.cborData }
215215

216216
type mockStakeDeregistrationCert struct {
217-
common.StakeDeregistrationCertificate
218217
cborData []byte
218+
common.StakeDeregistrationCertificate
219219
}
220220

221221
func (m *mockStakeDeregistrationCert) Cbor() []byte { return m.cborData }
@@ -251,10 +251,10 @@ func TestFilterByAddress(t *testing.T) {
251251
testStakeAddress, _ := bech32.Encode("stake", convData)
252252

253253
tests := []struct {
254-
name string
255-
filterAddress string
256254
outputAddr common.Address
257255
cert ledger.Certificate
256+
name string
257+
filterAddress string
258258
shouldMatch bool
259259
}{
260260
{

input/chainsync/block_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ func (m MockIssuerVkey) Hash() []byte {
2222

2323
// MockBlockHeader implements BlockHeader interface
2424
type MockBlockHeader struct {
25-
hash common.Blake2b256
26-
prevHash common.Blake2b256
25+
era common.Era
26+
cborBytes []byte
2727
blockNumber uint64
2828
slotNumber uint64
29-
issuerVkey common.IssuerVkey
3029
blockBodySize uint64
31-
era common.Era
32-
cborBytes []byte
30+
hash common.Blake2b256
31+
prevHash common.Blake2b256
32+
issuerVkey common.IssuerVkey
3333
}
3434

3535
func (m MockBlockHeader) Hash() common.Blake2b256 {
@@ -113,11 +113,11 @@ func (m MockBlock) IsConway() bool {
113113
func TestNewBlockContext(t *testing.T) {
114114
testCases := []struct {
115115
name string
116-
block MockBlock
117-
networkMagic uint32
118116
expectedEra string
117+
block MockBlock
119118
expectedBlock uint64
120119
expectedSlot uint64
120+
networkMagic uint32
121121
}{
122122
{
123123
name: "Shelley Era Block",
@@ -230,9 +230,9 @@ func TestNewBlockContext(t *testing.T) {
230230
func TestNewBlockContextEdgeCases(t *testing.T) {
231231
testCases := []struct {
232232
name string
233+
expectedEra string
233234
block MockBlock
234235
networkMagic uint32
235-
expectedEra string
236236
}{
237237
{
238238
name: "Zero Values",

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: 15 additions & 0 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 (
@@ -73,6 +86,8 @@ func TestHandleRollBackward(t *testing.T) {
7386
assert.Equal(t, "0102030405", c.status.BlockHash)
7487
assert.Equal(t, uint64(67890), c.status.TipSlotNumber)
7588
assert.Equal(t, "060708090a", c.status.TipBlockHash)
89+
// New: Check EpochNumber (Byron era: 12345/21600 = 0)
90+
assert.Equal(t, uint64(0), c.status.EpochNumber)
7691
}
7792

7893
func TestGetKupoClient(t *testing.T) {

internal/config/config.go

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,71 @@ import (
2424
"gopkg.in/yaml.v2"
2525
)
2626

27+
// ByronGenesisConfig holds Byron era genesis parameters
28+
type ByronGenesisConfig struct {
29+
EndSlot *uint64 `yaml:"end_slot"`
30+
Epochs *uint64 `yaml:"epochs"`
31+
EpochLength uint64 `yaml:"epoch_length"`
32+
ByronSlotsPerEpoch uint64 `yaml:"byron_slots_per_epoch"`
33+
}
34+
35+
// ShelleyGenesisConfig holds Shelley era genesis parameters
36+
type ShelleyGenesisConfig struct {
37+
EpochLength uint64 `yaml:"epoch_length"`
38+
}
39+
40+
// ShelleyTransEpoch holds transition slot/epoch info
41+
type ShelleyTransEpoch struct {
42+
FirstShelleySlot uint64 `yaml:"first_shelley_slot"`
43+
FirstShelleyEpoch uint64 `yaml:"first_shelley_epoch"`
44+
}
45+
46+
// populateByronGenesis sets defaults and validates ByronGenesisConfig
47+
func (c *Config) populateByronGenesis() {
48+
cfg := &c.ByronGenesis
49+
// Apply defaults only when values are truly unset. Zero values are valid for Byron-less networks.
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+
// Validation after defaults was redundant; any required fields are now set.
65+
}
66+
67+
// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig
68+
func (c *Config) populateShelleyGenesis() {
69+
cfg := &c.ShelleyGenesis
70+
if cfg.EpochLength == 0 {
71+
cfg.EpochLength = 432000
72+
}
73+
}
74+
2775
const (
2876
DefaultInputPlugin = "chainsync"
2977
DefaultOutputPlugin = "log"
3078
)
3179

3280
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:"-"`
81+
ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"`
82+
Plugin map[string]map[string]map[any]any `yaml:"plugins"`
83+
Logging LoggingConfig `yaml:"logging"`
84+
ConfigFile string `yaml:"-"`
85+
Input string `yaml:"input" envconfig:"INPUT"`
86+
Output string `yaml:"output" envconfig:"OUTPUT"`
87+
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
88+
Api ApiConfig `yaml:"api"`
89+
Debug DebugConfig `yaml:"debug"`
90+
ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"`
91+
Version bool `yaml:"-"`
4292
}
4393

4494
type ApiConfig struct {
@@ -71,6 +121,14 @@ var globalConfig = &Config{
71121
Input: DefaultInputPlugin,
72122
Output: DefaultOutputPlugin,
73123
KupoUrl: "",
124+
ByronGenesis: ByronGenesisConfig{
125+
EpochLength: 21600,
126+
EndSlot: func() *uint64 { v := uint64(4492799); return &v }(),
127+
Epochs: func() *uint64 { v := uint64(208); return &v }(),
128+
},
129+
ShelleyGenesis: ShelleyGenesisConfig{
130+
EpochLength: 432000,
131+
},
74132
}
75133

76134
func (c *Config) Load(configFile string) error {
@@ -86,12 +144,13 @@ func (c *Config) Load(configFile string) error {
86144
}
87145
}
88146
// 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
91147
err := envconfig.Process("dummy", c)
92148
if err != nil {
93149
return fmt.Errorf("error processing environment: %w", err)
94150
}
151+
// Populate Byron and Shelley genesis configs (from nview)
152+
c.populateByronGenesis()
153+
c.populateShelleyGenesis()
95154
return nil
96155
}
97156

0 commit comments

Comments
 (0)