Skip to content

Commit be7c575

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

File tree

5 files changed

+144
-25
lines changed

5 files changed

+144
-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: 85 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,83 @@ import (
2425
"gopkg.in/yaml.v2"
2526
)
2627

28+
// ByronGenesisConfig holds Byron era genesis parameters
29+
type ByronGenesisConfig struct {
30+
EndSlot *uint64 `yaml:"end_slot"`
31+
Epochs *uint64 `yaml:"epochs"`
32+
EpochLength uint64 `yaml:"epoch_length"`
33+
ByronSlotsPerEpoch uint64 `yaml:"byron_slots_per_epoch"`
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(
69+
"ByronGenesisConfig: EndSlot must be set (can be zero for Byron-less networks)",
70+
)
71+
}
72+
if cfg.Epochs == nil {
73+
return errors.New(
74+
"ByronGenesisConfig: Epochs must be set (can be zero for Byron-less networks)",
75+
)
76+
}
77+
return nil
78+
}
79+
80+
// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig
81+
func (c *Config) populateShelleyGenesis() {
82+
cfg := &c.ShelleyGenesis
83+
if cfg.EpochLength == 0 {
84+
cfg.EpochLength = 432000
85+
}
86+
}
87+
2788
const (
2889
DefaultInputPlugin = "chainsync"
2990
DefaultOutputPlugin = "log"
3091
)
3192

3293
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:"-"`
94+
ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"`
95+
Plugin map[string]map[string]map[any]any `yaml:"plugins"`
96+
Logging LoggingConfig `yaml:"logging"`
97+
ConfigFile string `yaml:"-"`
98+
Input string `yaml:"input" envconfig:"INPUT"`
99+
Output string `yaml:"output" envconfig:"OUTPUT"`
100+
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
101+
Api ApiConfig `yaml:"api"`
102+
Debug DebugConfig `yaml:"debug"`
103+
ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"`
104+
Version bool `yaml:"-"`
42105
}
43106

44107
type ApiConfig struct {
@@ -71,6 +134,14 @@ var globalConfig = &Config{
71134
Input: DefaultInputPlugin,
72135
Output: DefaultOutputPlugin,
73136
KupoUrl: "",
137+
ByronGenesis: ByronGenesisConfig{
138+
EpochLength: 21600,
139+
EndSlot: func() *uint64 { v := uint64(4492799); return &v }(),
140+
Epochs: func() *uint64 { v := uint64(208); return &v }(),
141+
},
142+
ShelleyGenesis: ShelleyGenesisConfig{
143+
EpochLength: 432000,
144+
},
74145
}
75146

76147
func (c *Config) Load(configFile string) error {
@@ -86,12 +157,15 @@ func (c *Config) Load(configFile string) error {
86157
}
87158
}
88159
// 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
91160
err := envconfig.Process("dummy", c)
92161
if err != nil {
93162
return fmt.Errorf("error processing environment: %w", err)
94163
}
164+
// Populate Byron and Shelley genesis configs (from nview)
165+
if err := c.populateByronGenesis(); err != nil {
166+
return fmt.Errorf("error populating Byron genesis: %w", err)
167+
}
168+
c.populateShelleyGenesis()
95169
return nil
96170
}
97171

0 commit comments

Comments
 (0)