Skip to content

Commit 32d859b

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

File tree

8 files changed

+146
-37
lines changed

8 files changed

+146
-37
lines changed

api/api_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import (
55
"net/http/httptest"
66
"testing"
77

8+
"github.com/stretchr/testify/assert"
9+
810
"github.com/blinklabs-io/adder/api"
911
"github.com/blinklabs-io/adder/output/push"
10-
"github.com/stretchr/testify/assert"
1112
)
1213

1314
func TestRouteRegistration(t *testing.T) {

filter/chainsync/chainsync_test.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ import (
1919
"testing"
2020
"time"
2121

22-
"github.com/blinklabs-io/adder/event"
2322
"github.com/blinklabs-io/gouroboros/cbor"
2423
"github.com/blinklabs-io/gouroboros/ledger"
2524
"github.com/blinklabs-io/gouroboros/ledger/common"
2625
"github.com/blinklabs-io/plutigo/data"
2726
"github.com/btcsuite/btcd/btcutil/bech32"
2827
"github.com/stretchr/testify/assert"
2928
"github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano"
29+
30+
"github.com/blinklabs-io/adder/event"
3031
)
3132

3233
// MockLogger is a mock implementation of the plugin.Logger interface
@@ -92,10 +93,10 @@ func (m *MockAddress) UnmarshalCBOR(data []byte) error {
9293
// MockOutput is a mock implementation of the TransactionOutput interface
9394
type MockOutput struct {
9495
address ledger.Address
95-
amount uint64
96+
scriptRef common.Script
9697
assets *common.MultiAsset[common.MultiAssetTypeOutput]
9798
datum *common.Datum
98-
scriptRef common.Script
99+
amount uint64
99100
}
100101

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

208209
// Mock certificate implementations
209210
type mockStakeDelegationCert struct {
210-
common.StakeDelegationCertificate
211211
cborData []byte
212+
common.StakeDelegationCertificate
212213
}
213214

214215
func (m *mockStakeDelegationCert) Cbor() []byte { return m.cborData }
215216

216217
type mockStakeDeregistrationCert struct {
217-
common.StakeDeregistrationCertificate
218218
cborData []byte
219+
common.StakeDeregistrationCertificate
219220
}
220221

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

253254
tests := []struct {
254-
name string
255-
filterAddress string
256255
outputAddr common.Address
257256
cert ledger.Certificate
257+
name string
258+
filterAddress string
258259
shouldMatch bool
259260
}{
260261
{

filter/chainsync/plugin_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/stretchr/testify/assert"
8+
79
"github.com/blinklabs-io/adder/event"
810
"github.com/blinklabs-io/adder/plugin"
9-
"github.com/stretchr/testify/assert"
1011
)
1112

1213
func TestPluginRegistration(t *testing.T) {

input/chainsync/block_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ package chainsync
33
import (
44
"testing"
55

6-
"github.com/blinklabs-io/adder/event"
76
"github.com/blinklabs-io/gouroboros/ledger/common"
87
"github.com/stretchr/testify/assert"
98
utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano"
9+
10+
"github.com/blinklabs-io/adder/event"
1011
)
1112

1213
// MockIssuerVkey to implement IssuerVkey interface
@@ -22,14 +23,14 @@ func (m MockIssuerVkey) Hash() []byte {
2223

2324
// MockBlockHeader implements BlockHeader interface
2425
type MockBlockHeader struct {
25-
hash common.Blake2b256
26-
prevHash common.Blake2b256
26+
era common.Era
27+
cborBytes []byte
2728
blockNumber uint64
2829
slotNumber uint64
29-
issuerVkey common.IssuerVkey
3030
blockBodySize uint64
31-
era common.Era
32-
cborBytes []byte
31+
hash common.Blake2b256
32+
prevHash common.Blake2b256
33+
issuerVkey common.IssuerVkey
3334
}
3435

3536
func (m MockBlockHeader) Hash() common.Blake2b256 {
@@ -113,11 +114,11 @@ func (m MockBlock) IsConway() bool {
113114
func TestNewBlockContext(t *testing.T) {
114115
testCases := []struct {
115116
name string
116-
block MockBlock
117-
networkMagic uint32
118117
expectedEra string
118+
block MockBlock
119119
expectedBlock uint64
120120
expectedSlot uint64
121+
networkMagic uint32
121122
}{
122123
{
123124
name: "Shelley Era Block",
@@ -230,9 +231,9 @@ func TestNewBlockContext(t *testing.T) {
230231
func TestNewBlockContextEdgeCases(t *testing.T) {
231232
testCases := []struct {
232233
name string
234+
expectedEra string
233235
block MockBlock
234236
networkMagic uint32
235-
expectedEra string
236237
}{
237238
{
238239
name: "Zero Values",

input/chainsync/chainsync.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,51 @@ import (
2828
"time"
2929

3030
"github.com/SundaeSwap-finance/kugo"
31-
"github.com/SundaeSwap-finance/ogmigo/v6/ouroboros/chainsync"
3231
"github.com/blinklabs-io/adder/event"
3332
"github.com/blinklabs-io/adder/internal/config"
3433
"github.com/blinklabs-io/adder/internal/logging"
3534
"github.com/blinklabs-io/adder/plugin"
3635
ouroboros "github.com/blinklabs-io/gouroboros"
3736
"github.com/blinklabs-io/gouroboros/ledger"
38-
"github.com/blinklabs-io/gouroboros/protocol/blockfetch"
37+
blockfetch "github.com/blinklabs-io/gouroboros/protocol/blockfetch"
3938
ochainsync "github.com/blinklabs-io/gouroboros/protocol/chainsync"
4039
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
4140
)
4241

42+
// EpochFromSlot derives an epoch from a slot using Byron/Shelley genesis params.
43+
// Byron slots: 0..EndSlot inclusive. Explicit zero EndSlot/Epochs means no Byron era.
44+
// Zero epoch length in either era yields a safe fallback (0 Byron / starting Shelley epoch).
45+
func EpochFromSlot(slot uint64) uint64 {
46+
cfg := config.GetConfig()
47+
byron := cfg.ByronGenesis
48+
shelley := cfg.ShelleyGenesis
49+
50+
endSlot := func() uint64 {
51+
if byron.EndSlot != nil {
52+
return *byron.EndSlot
53+
}
54+
return 0
55+
}()
56+
byronEpochs := func() uint64 {
57+
if byron.Epochs != nil {
58+
return *byron.Epochs
59+
}
60+
return 0
61+
}()
62+
if slot <= endSlot {
63+
if byron.EpochLength == 0 {
64+
return 0 // avoid div by zero
65+
}
66+
return slot / byron.EpochLength
67+
}
68+
shelleyStartEpoch := byronEpochs
69+
shelleyStartSlot := endSlot + 1
70+
if shelley.EpochLength == 0 {
71+
return shelleyStartEpoch // avoid div by zero
72+
}
73+
return shelleyStartEpoch + (slot-shelleyStartSlot)/shelley.EpochLength
74+
}
75+
4376
const (
4477
// Size of cache for recent chainsync cursors
4578
cursorCacheSize = 20
@@ -84,6 +117,7 @@ type ChainSyncStatus struct {
84117
TipBlockHash string
85118
SlotNumber uint64
86119
BlockNumber uint64
120+
EpochNumber uint64
87121
TipSlotNumber uint64
88122
TipReached bool
89123
}
@@ -522,6 +556,7 @@ func (c *ChainSync) updateStatus(
522556
c.status.SlotNumber = slotNumber
523557
c.status.BlockNumber = blockNumber
524558
c.status.BlockHash = blockHash
559+
c.status.EpochNumber = EpochFromSlot(slotNumber)
525560
c.status.TipSlotNumber = tipSlotNumber
526561
c.status.TipBlockHash = tipBlockHash
527562
if c.statusUpdateFunc != nil {
@@ -621,9 +656,9 @@ func resolveTransactionInputs(
621656
)
622657
defer cancel()
623658

624-
matches, err := k.Matches(ctx,
625-
kugo.TxOut(chainsync.NewTxID(txId, txIndex)),
626-
)
659+
// Create a simple transaction identifier
660+
txID := fmt.Sprintf("%s#%d", txId, txIndex)
661+
matches, err := k.Matches(ctx, kugo.Transaction(txID))
627662
if err != nil {
628663
if errors.Is(err, context.DeadlineExceeded) {
629664
return nil, fmt.Errorf(

input/chainsync/chainsync_test.go

Lines changed: 17 additions & 1 deletion
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 (
@@ -10,11 +23,12 @@ import (
1023
"time"
1124

1225
"github.com/SundaeSwap-finance/kugo"
13-
"github.com/blinklabs-io/adder/event"
1426
"github.com/blinklabs-io/gouroboros/protocol/chainsync"
1527
ocommon "github.com/blinklabs-io/gouroboros/protocol/common"
1628
"github.com/stretchr/testify/assert"
1729
"github.com/stretchr/testify/require"
30+
31+
"github.com/blinklabs-io/adder/event"
1832
)
1933

2034
func TestHandleRollBackward(t *testing.T) {
@@ -73,6 +87,8 @@ func TestHandleRollBackward(t *testing.T) {
7387
assert.Equal(t, "0102030405", c.status.BlockHash)
7488
assert.Equal(t, uint64(67890), c.status.TipSlotNumber)
7589
assert.Equal(t, "060708090a", c.status.TipBlockHash)
90+
// New: Check EpochNumber (Byron era: 12345/21600 = 0)
91+
assert.Equal(t, uint64(0), c.status.EpochNumber)
7692
}
7793

7894
func TestGetKupoClient(t *testing.T) {

internal/config/config.go

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,65 @@ 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+
// populateByronGenesis sets defaults and validates ByronGenesisConfig
41+
func (c *Config) populateByronGenesis() {
42+
cfg := &c.ByronGenesis
43+
// Apply defaults only when values are truly unset. Zero values are valid for Byron-less networks.
44+
if cfg.EpochLength == 0 {
45+
cfg.EpochLength = 21600
46+
}
47+
if cfg.ByronSlotsPerEpoch == 0 {
48+
cfg.ByronSlotsPerEpoch = cfg.EpochLength
49+
}
50+
if cfg.EndSlot == nil {
51+
defaultEndSlot := uint64(4492799)
52+
cfg.EndSlot = &defaultEndSlot
53+
}
54+
if cfg.Epochs == nil {
55+
defaultEpochs := uint64(208)
56+
cfg.Epochs = &defaultEpochs
57+
}
58+
// Validation after defaults was redundant; any required fields are now set.
59+
}
60+
61+
// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig
62+
func (c *Config) populateShelleyGenesis() {
63+
cfg := &c.ShelleyGenesis
64+
if cfg.EpochLength == 0 {
65+
cfg.EpochLength = 432000
66+
}
67+
}
68+
2769
const (
2870
DefaultInputPlugin = "chainsync"
2971
DefaultOutputPlugin = "log"
3072
)
3173

3274
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:"-"`
75+
ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"`
76+
Plugin map[string]map[string]map[any]any `yaml:"plugins"`
77+
Logging LoggingConfig `yaml:"logging"`
78+
ConfigFile string `yaml:"-"`
79+
Input string `yaml:"input" envconfig:"INPUT"`
80+
Output string `yaml:"output" envconfig:"OUTPUT"`
81+
KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"`
82+
Api ApiConfig `yaml:"api"`
83+
Debug DebugConfig `yaml:"debug"`
84+
ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"`
85+
Version bool `yaml:"-"`
4286
}
4387

4488
type ApiConfig struct {
@@ -71,6 +115,14 @@ var globalConfig = &Config{
71115
Input: DefaultInputPlugin,
72116
Output: DefaultOutputPlugin,
73117
KupoUrl: "",
118+
ByronGenesis: ByronGenesisConfig{
119+
EpochLength: 21600,
120+
EndSlot: func() *uint64 { v := uint64(4492799); return &v }(),
121+
Epochs: func() *uint64 { v := uint64(208); return &v }(),
122+
},
123+
ShelleyGenesis: ShelleyGenesisConfig{
124+
EpochLength: 432000,
125+
},
74126
}
75127

76128
func (c *Config) Load(configFile string) error {
@@ -86,12 +138,13 @@ func (c *Config) Load(configFile string) error {
86138
}
87139
}
88140
// 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
91141
err := envconfig.Process("dummy", c)
92142
if err != nil {
93143
return fmt.Errorf("error processing environment: %w", err)
94144
}
145+
// Populate Byron and Shelley genesis configs (from nview)
146+
c.populateByronGenesis()
147+
c.populateShelleyGenesis()
95148
return nil
96149
}
97150

output/push/api_routes_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import (
66
"strings"
77
"testing"
88

9-
"github.com/blinklabs-io/adder/api"
10-
"github.com/blinklabs-io/adder/output/push"
119
"github.com/gin-gonic/gin"
1210
"github.com/stretchr/testify/assert"
11+
12+
"github.com/blinklabs-io/adder/api"
13+
"github.com/blinklabs-io/adder/output/push"
1314
)
1415

1516
func setupRouter() *gin.Engine {

0 commit comments

Comments
 (0)