diff --git a/README.md b/README.md index cdaa482..966b4fb 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,32 @@ variable. For example, the `-input` option has the `INPUT` environment variable, the `-input-chainsync-address` option has the `INPUT_CHAINSYNC_ADDRESS` environment variable, and `-output` has `OUTPUT`. +### Environment Variables + +Core configuration options can be set using environment variables: + +- `INPUT` - Input plugin to use (default: "chainsync") +- `OUTPUT` - Output plugin to use (default: "log") +- `KUPO_URL` - URL for Kupo service integration +- `LOGGING_LEVEL` - Log level (default: "info") +- `API_ADDRESS` - API server listen address (default: "0.0.0.0") +- `API_PORT` - API server port (default: 8080) +- `DEBUG_ADDRESS` - Debug server address (default: "localhost") +- `DEBUG_PORT` - Debug server port (default: 0) + +Genesis configuration can also be controlled via environment variables: + +**Network Transition:** +- `SHELLEY_TRANS_EPOCH` - Epoch number when Shelley era begins (default: 208 for mainnet) + +**Byron Genesis:** +- `BYRON_GENESIS_END_SLOT` - End slot for Byron era +- `BYRON_GENESIS_EPOCH_LENGTH` - Slot length of Byron epochs (default: 21600) +- `BYRON_GENESIS_BYRON_SLOTS_PER_EPOCH` - Byron slots per epoch + +**Shelley Genesis:** +- `SHELLEY_GENESIS_EPOCH_LENGTH` - Slot length of Shelley epochs (default: 432000) + You can also specify each option in the config file. ```yaml diff --git a/api/api_test.go b/api/api_test.go index 10a43ba..65523d9 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -5,9 +5,10 @@ import ( "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/blinklabs-io/adder/api" "github.com/blinklabs-io/adder/output/push" - "github.com/stretchr/testify/assert" ) func TestRouteRegistration(t *testing.T) { diff --git a/filter/chainsync/chainsync_test.go b/filter/chainsync/chainsync_test.go index f093953..818da2b 100644 --- a/filter/chainsync/chainsync_test.go +++ b/filter/chainsync/chainsync_test.go @@ -19,7 +19,6 @@ import ( "testing" "time" - "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -27,6 +26,8 @@ import ( "github.com/btcsuite/btcd/btcutil/bech32" "github.com/stretchr/testify/assert" "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" + + "github.com/blinklabs-io/adder/event" ) // MockLogger is a mock implementation of the plugin.Logger interface @@ -92,10 +93,10 @@ func (m *MockAddress) UnmarshalCBOR(data []byte) error { // MockOutput is a mock implementation of the TransactionOutput interface type MockOutput struct { address ledger.Address - amount uint64 + scriptRef common.Script assets *common.MultiAsset[common.MultiAssetTypeOutput] datum *common.Datum - scriptRef common.Script + amount uint64 } func (m MockOutput) Address() ledger.Address { @@ -207,15 +208,15 @@ func TestChainSync_OutputChan(t *testing.T) { // Mock certificate implementations type mockStakeDelegationCert struct { - common.StakeDelegationCertificate cborData []byte + common.StakeDelegationCertificate } func (m *mockStakeDelegationCert) Cbor() []byte { return m.cborData } type mockStakeDeregistrationCert struct { - common.StakeDeregistrationCertificate cborData []byte + common.StakeDeregistrationCertificate } func (m *mockStakeDeregistrationCert) Cbor() []byte { return m.cborData } @@ -251,10 +252,10 @@ func TestFilterByAddress(t *testing.T) { testStakeAddress, _ := bech32.Encode("stake", convData) tests := []struct { - name string - filterAddress string outputAddr common.Address cert ledger.Certificate + name string + filterAddress string shouldMatch bool }{ { diff --git a/filter/chainsync/plugin_test.go b/filter/chainsync/plugin_test.go index 91faa23..290c12d 100644 --- a/filter/chainsync/plugin_test.go +++ b/filter/chainsync/plugin_test.go @@ -4,9 +4,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/adder/plugin" - "github.com/stretchr/testify/assert" ) func TestPluginRegistration(t *testing.T) { diff --git a/input/chainsync/block_test.go b/input/chainsync/block_test.go index 854ff43..41b64a2 100644 --- a/input/chainsync/block_test.go +++ b/input/chainsync/block_test.go @@ -3,10 +3,11 @@ package chainsync import ( "testing" - "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/stretchr/testify/assert" utxorpc "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" + + "github.com/blinklabs-io/adder/event" ) // MockIssuerVkey to implement IssuerVkey interface @@ -22,14 +23,14 @@ func (m MockIssuerVkey) Hash() []byte { // MockBlockHeader implements BlockHeader interface type MockBlockHeader struct { - hash common.Blake2b256 - prevHash common.Blake2b256 + era common.Era + cborBytes []byte blockNumber uint64 slotNumber uint64 - issuerVkey common.IssuerVkey blockBodySize uint64 - era common.Era - cborBytes []byte + hash common.Blake2b256 + prevHash common.Blake2b256 + issuerVkey common.IssuerVkey } func (m MockBlockHeader) Hash() common.Blake2b256 { @@ -113,11 +114,11 @@ func (m MockBlock) IsConway() bool { func TestNewBlockContext(t *testing.T) { testCases := []struct { name string - block MockBlock - networkMagic uint32 expectedEra string + block MockBlock expectedBlock uint64 expectedSlot uint64 + networkMagic uint32 }{ { name: "Shelley Era Block", @@ -230,9 +231,9 @@ func TestNewBlockContext(t *testing.T) { func TestNewBlockContextEdgeCases(t *testing.T) { testCases := []struct { name string + expectedEra string block MockBlock networkMagic uint32 - expectedEra string }{ { name: "Zero Values", diff --git a/input/chainsync/chainsync.go b/input/chainsync/chainsync.go index 8f019c1..5e04ca1 100644 --- a/input/chainsync/chainsync.go +++ b/input/chainsync/chainsync.go @@ -28,18 +28,52 @@ import ( "time" "github.com/SundaeSwap-finance/kugo" - "github.com/SundaeSwap-finance/ogmigo/v6/ouroboros/chainsync" "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/adder/internal/config" "github.com/blinklabs-io/adder/internal/logging" "github.com/blinklabs-io/adder/plugin" ouroboros "github.com/blinklabs-io/gouroboros" "github.com/blinklabs-io/gouroboros/ledger" - "github.com/blinklabs-io/gouroboros/protocol/blockfetch" + blockfetch "github.com/blinklabs-io/gouroboros/protocol/blockfetch" ochainsync "github.com/blinklabs-io/gouroboros/protocol/chainsync" ocommon "github.com/blinklabs-io/gouroboros/protocol/common" ) +// EpochFromSlot derives an epoch from a slot using Byron/Shelley genesis params. +// Byron slots: 0..EndSlot inclusive. Explicit zero EndSlot/ShelleyTransEpoch means no Byron era. +// Zero epoch length in either era yields a safe fallback (0 Byron / starting Shelley epoch). +func EpochFromSlot(slot uint64) uint64 { + cfg := config.GetConfig() + byron := cfg.ByronGenesis + shelley := cfg.ShelleyGenesis + + endSlot := func() uint64 { + if byron.EndSlot != nil { + return *byron.EndSlot + } + return 0 + }() + shelleyTransEpoch := func() uint64 { + if cfg.ShelleyTransEpoch >= 0 { + //nolint:gosec // ShelleyTransEpoch is controlled config, safe conversion + return uint64(cfg.ShelleyTransEpoch) + } + return 0 + }() + if slot <= endSlot { + if byron.EpochLength == 0 { + return 0 // avoid div by zero + } + return slot / byron.EpochLength + } + shelleyStartEpoch := shelleyTransEpoch + shelleyStartSlot := endSlot + 1 + if shelley.EpochLength == 0 { + return shelleyStartEpoch // avoid div by zero + } + return shelleyStartEpoch + (slot-shelleyStartSlot)/shelley.EpochLength +} + const ( // Size of cache for recent chainsync cursors cursorCacheSize = 20 @@ -84,6 +118,7 @@ type ChainSyncStatus struct { TipBlockHash string SlotNumber uint64 BlockNumber uint64 + EpochNumber uint64 TipSlotNumber uint64 TipReached bool } @@ -522,6 +557,7 @@ func (c *ChainSync) updateStatus( c.status.SlotNumber = slotNumber c.status.BlockNumber = blockNumber c.status.BlockHash = blockHash + c.status.EpochNumber = EpochFromSlot(slotNumber) c.status.TipSlotNumber = tipSlotNumber c.status.TipBlockHash = tipBlockHash if c.statusUpdateFunc != nil { @@ -621,9 +657,9 @@ func resolveTransactionInputs( ) defer cancel() - matches, err := k.Matches(ctx, - kugo.TxOut(chainsync.NewTxID(txId, txIndex)), - ) + // Create a simple transaction identifier + txID := fmt.Sprintf("%d@%s", txIndex, txId) + matches, err := k.Matches(ctx, kugo.Transaction(txID)) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil, fmt.Errorf( diff --git a/input/chainsync/chainsync_test.go b/input/chainsync/chainsync_test.go index 89d26ce..edbb00e 100644 --- a/input/chainsync/chainsync_test.go +++ b/input/chainsync/chainsync_test.go @@ -1,3 +1,16 @@ +// Copyright 2025 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. package chainsync import ( @@ -10,11 +23,12 @@ import ( "time" "github.com/SundaeSwap-finance/kugo" - "github.com/blinklabs-io/adder/event" "github.com/blinklabs-io/gouroboros/protocol/chainsync" ocommon "github.com/blinklabs-io/gouroboros/protocol/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/blinklabs-io/adder/event" ) func TestHandleRollBackward(t *testing.T) { @@ -26,7 +40,7 @@ func TestHandleRollBackward(t *testing.T) { // Define test data point := ocommon.Point{ - Slot: 12345, + Slot: 123456, Hash: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, } tip := chainsync.Tip{ @@ -64,7 +78,7 @@ func TestHandleRollBackward(t *testing.T) { } // Verify that the status was updated correctly - assert.Equal(t, uint64(12345), c.status.SlotNumber) + assert.Equal(t, uint64(123456), c.status.SlotNumber) assert.Equal( t, uint64(0), @@ -73,6 +87,8 @@ func TestHandleRollBackward(t *testing.T) { assert.Equal(t, "0102030405", c.status.BlockHash) assert.Equal(t, uint64(67890), c.status.TipSlotNumber) assert.Equal(t, "060708090a", c.status.TipBlockHash) + // New: Check EpochNumber (Byron era: 123456/21600 = 5) + assert.Equal(t, uint64(5), c.status.EpochNumber) } func TestGetKupoClient(t *testing.T) { diff --git a/internal/config/config.go b/internal/config/config.go index 21e8961..20e0a02 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -24,21 +24,67 @@ import ( "gopkg.in/yaml.v2" ) +// ByronGenesisConfig holds Byron era genesis parameters +type ByronGenesisConfig struct { + EndSlot *uint64 `yaml:"end_slot" envconfig:"BYRON_GENESIS_END_SLOT"` + EpochLength uint64 `yaml:"epoch_length" envconfig:"BYRON_GENESIS_EPOCH_LENGTH"` + ByronSlotsPerEpoch uint64 `yaml:"byron_slots_per_epoch" envconfig:"BYRON_GENESIS_BYRON_SLOTS_PER_EPOCH"` +} + +// ShelleyGenesisConfig holds Shelley era genesis parameters +type ShelleyGenesisConfig struct { + EpochLength uint64 `yaml:"epoch_length" envconfig:"SHELLEY_GENESIS_EPOCH_LENGTH"` +} + +// populateByronGenesis sets defaults and validates ByronGenesisConfig +func (c *Config) populateByronGenesis() { + cfg := &c.ByronGenesis + // Apply defaults only when values are truly unset. Zero values are valid for Byron-less networks. + if cfg.EpochLength == 0 { + cfg.EpochLength = 21600 + } + if cfg.ByronSlotsPerEpoch == 0 { + cfg.ByronSlotsPerEpoch = cfg.EpochLength + } + if cfg.EndSlot == nil { + defaultEndSlot := uint64(4492799) + cfg.EndSlot = &defaultEndSlot + } +} + +// populateShelleyTransEpoch sets the Shelley transition epoch +func (c *Config) populateShelleyTransEpoch() { + if c.ShelleyTransEpoch == -1 { + c.ShelleyTransEpoch = 208 // Default to mainnet + } +} + +// populateShelleyGenesis sets defaults and validates ShelleyGenesisConfig +func (c *Config) populateShelleyGenesis() { + cfg := &c.ShelleyGenesis + if cfg.EpochLength == 0 { + cfg.EpochLength = 432000 + } +} + const ( DefaultInputPlugin = "chainsync" DefaultOutputPlugin = "log" ) type Config struct { - Plugin map[string]map[string]map[any]any `yaml:"plugins"` - Logging LoggingConfig `yaml:"logging"` - ConfigFile string `yaml:"-"` - Input string `yaml:"input" envconfig:"INPUT"` - Output string `yaml:"output" envconfig:"OUTPUT"` - KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"` - Api ApiConfig `yaml:"api"` - Debug DebugConfig `yaml:"debug"` - Version bool `yaml:"-"` + ByronGenesis ByronGenesisConfig `yaml:"byron_genesis" envconfig:"BYRON_GENESIS"` + Plugin map[string]map[string]map[any]any `yaml:"plugins" envconfig:"PLUGINS"` + Logging LoggingConfig `yaml:"logging" envconfig:"LOGGING"` + ConfigFile string `yaml:"-"` + Input string `yaml:"input" envconfig:"INPUT"` + Output string `yaml:"output" envconfig:"OUTPUT"` + KupoUrl string `yaml:"kupo_url" envconfig:"KUPO_URL"` + Api ApiConfig `yaml:"api" envconfig:"API"` + Debug DebugConfig `yaml:"debug" envconfig:"DEBUG"` + ShelleyGenesis ShelleyGenesisConfig `yaml:"shelley_genesis" envconfig:"SHELLEY_GENESIS"` + ShelleyTransEpoch int32 `yaml:"shelley_trans_epoch" envconfig:"SHELLEY_TRANS_EPOCH"` + Version bool `yaml:"-"` } type ApiConfig struct { @@ -68,9 +114,17 @@ var globalConfig = &Config{ ListenAddress: "localhost", ListenPort: 0, }, - Input: DefaultInputPlugin, - Output: DefaultOutputPlugin, - KupoUrl: "", + Input: DefaultInputPlugin, + Output: DefaultOutputPlugin, + KupoUrl: "", + ShelleyTransEpoch: -1, // Use -1 to indicate unset, will be populated later + ByronGenesis: ByronGenesisConfig{ + EpochLength: 21600, + EndSlot: func() *uint64 { v := uint64(4492799); return &v }(), + }, + ShelleyGenesis: ShelleyGenesisConfig{ + EpochLength: 432000, + }, } func (c *Config) Load(configFile string) error { @@ -86,12 +140,14 @@ func (c *Config) Load(configFile string) error { } } // Load config values from environment variables - // We use "dummy" as the app name here to (mostly) prevent picking up env - // vars that we hadn't explicitly specified in annotations above err := envconfig.Process("dummy", c) if err != nil { return fmt.Errorf("error processing environment: %w", err) } + // Populate Byron and Shelley genesis configs and transition epoch + c.populateByronGenesis() + c.populateShelleyGenesis() + c.populateShelleyTransEpoch() return nil } diff --git a/output/push/api_routes_test.go b/output/push/api_routes_test.go index a254b99..574812b 100644 --- a/output/push/api_routes_test.go +++ b/output/push/api_routes_test.go @@ -6,10 +6,11 @@ import ( "strings" "testing" - "github.com/blinklabs-io/adder/api" - "github.com/blinklabs-io/adder/output/push" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" + + "github.com/blinklabs-io/adder/api" + "github.com/blinklabs-io/adder/output/push" ) func setupRouter() *gin.Engine {