From 6a7701d737b644a06addbc67c82a89f45baf59c2 Mon Sep 17 00:00:00 2001 From: Aurora Gaffney Date: Wed, 30 Oct 2024 09:59:45 -0500 Subject: [PATCH] feat: protocol params tracking from genesis configs (#200) * split ledger config into separate type * basic era tracking * per-era functions for decoding and upgrading pparams * persist active pparams in DB --- go.mod | 4 +- go.sum | 8 +- node.go | 9 +- state/chain.go | 8 +- state/eras/allegra.go | 48 ++++++++++ state/eras/alonzo.go | 53 +++++++++++ state/eras/babbage.go | 48 ++++++++++ state/eras/byron.go | 22 +++++ state/eras/conway.go | 53 +++++++++++ state/eras/eras.go | 34 +++++++ state/eras/mary.go | 48 ++++++++++ state/eras/shelley.go | 48 ++++++++++ state/models/era.go | 48 ++++++++++ state/models/models.go | 2 + state/models/pparams.go | 27 ++++++ state/state.go | 198 ++++++++++++++++++++++++++++++---------- topology/topology.go | 2 +- 17 files changed, 597 insertions(+), 63 deletions(-) create mode 100644 state/eras/allegra.go create mode 100644 state/eras/alonzo.go create mode 100644 state/eras/babbage.go create mode 100644 state/eras/byron.go create mode 100644 state/eras/conway.go create mode 100644 state/eras/eras.go create mode 100644 state/eras/mary.go create mode 100644 state/eras/shelley.go create mode 100644 state/models/era.go create mode 100644 state/models/pparams.go diff --git a/go.mod b/go.mod index e6ba2fd..719d485 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.22 toolchain go1.22.6 require ( - github.com/blinklabs-io/gouroboros v0.102.0 - github.com/blinklabs-io/ouroboros-mock v0.3.4 + github.com/blinklabs-io/gouroboros v0.103.0 + github.com/blinklabs-io/ouroboros-mock v0.3.5 github.com/dgraph-io/badger/v4 v4.3.1 github.com/glebarez/sqlite v1.11.0 github.com/kelseyhightower/envconfig v1.4.0 diff --git a/go.sum b/go.sum index 4771d09..7a873e4 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,10 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/blinklabs-io/gouroboros v0.102.0 h1:SSw1SW5a/eunPJdwJHARur/dlGdTrLEq5ScRFZjhen0= -github.com/blinklabs-io/gouroboros v0.102.0/go.mod h1:okgJskD4KXG5itGM95gUl6J+rKbURzaP++HTeCokgkM= -github.com/blinklabs-io/ouroboros-mock v0.3.4 h1:codPfiI5vLeD6YdhKL5VwYSzy2N3Dsgx6xjcLsqFaJQ= -github.com/blinklabs-io/ouroboros-mock v0.3.4/go.mod h1:e/wgG1ZYVenroN2XEMXy7DgEfdmP7KXVRHIQKuh8E/0= +github.com/blinklabs-io/gouroboros v0.103.0 h1:mA/RJ7CeZEE41RQNeMoip/2BbcB1WC2O8Nd+92m4vHY= +github.com/blinklabs-io/gouroboros v0.103.0/go.mod h1:wjiNCbZ2uQy9DGfLCgEgqagHxNBAv5UYsOdRBgoi3SU= +github.com/blinklabs-io/ouroboros-mock v0.3.5 h1:/KWbSoH8Pjrd9uxOH7mVbI7XFsDCNW/O9FtLlvJDUpQ= +github.com/blinklabs-io/ouroboros-mock v0.3.5/go.mod h1:JtUQ3Luo22hCnGBxuxNp6JaUx63VxidxWwmcaVMremw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/node.go b/node.go index 74c68d6..9f8a67b 100644 --- a/node.go +++ b/node.go @@ -73,9 +73,12 @@ func (n *Node) Run() error { } // Load state state, err := state.NewLedgerState( - n.config.dataDir, - n.eventBus, - n.config.logger, + state.LedgerStateConfig{ + DataDir: n.config.dataDir, + EventBus: n.eventBus, + Logger: n.config.logger, + CardanoNodeConfig: n.config.cardanoNodeConfig, + }, ) if err != nil { return fmt.Errorf("failed to load state database: %w", err) diff --git a/state/chain.go b/state/chain.go index 52de803..9f98954 100644 --- a/state/chain.go +++ b/state/chain.go @@ -86,8 +86,8 @@ func (ci *ChainIterator) Next(blocking bool) (*ChainIteratorResult, error) { return nil, nil } // Wait for new block or a rollback - blockSubId, blockChan := ci.ls.eventBus.Subscribe(ChainBlockEventType) - rollbackSubId, rollbackChan := ci.ls.eventBus.Subscribe( + blockSubId, blockChan := ci.ls.config.EventBus.Subscribe(ChainBlockEventType) + rollbackSubId, rollbackChan := ci.ls.config.EventBus.Subscribe( ChainRollbackEventType, ) // Release read lock while we wait for new event @@ -111,7 +111,7 @@ func (ci *ChainIterator) Next(blocking bool) (*ChainIteratorResult, error) { ret.Point = rollbackData.Point ret.Rollback = true } - ci.ls.eventBus.Unsubscribe(ChainBlockEventType, blockSubId) - ci.ls.eventBus.Unsubscribe(ChainRollbackEventType, rollbackSubId) + ci.ls.config.EventBus.Unsubscribe(ChainBlockEventType, blockSubId) + ci.ls.config.EventBus.Unsubscribe(ChainRollbackEventType, rollbackSubId) return ret, nil } diff --git a/state/eras/allegra.go b/state/eras/allegra.go new file mode 100644 index 0000000..c50fe16 --- /dev/null +++ b/state/eras/allegra.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 eras + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/allegra" + "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/node/config/cardano" +) + +var AllegraEraDesc = EraDesc{ + Id: allegra.EraIdAllegra, + Name: allegra.EraNameAllegra, + DecodePParamsFunc: DecodePParamsAllegra, + HardForkFunc: HardForkAllegra, +} + +func DecodePParamsAllegra(data []byte) (any, error) { + var ret allegra.AllegraProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkAllegra(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + shelleyPParams, ok := prevPParams.(shelley.ShelleyProtocolParameters) + if !ok { + return nil, fmt.Errorf("previous PParams (%T) are not expected type", prevPParams) + } + ret := allegra.UpgradePParams(shelleyPParams) + return ret, nil +} diff --git a/state/eras/alonzo.go b/state/eras/alonzo.go new file mode 100644 index 0000000..ac7e203 --- /dev/null +++ b/state/eras/alonzo.go @@ -0,0 +1,53 @@ +// Copyright 2024 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 eras + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/alonzo" + "github.com/blinklabs-io/gouroboros/ledger/mary" + "github.com/blinklabs-io/node/config/cardano" +) + +var AlonzoEraDesc = EraDesc{ + Id: alonzo.EraIdAlonzo, + Name: alonzo.EraNameAlonzo, + DecodePParamsFunc: DecodePParamsAlonzo, + HardForkFunc: HardForkAlonzo, +} + +func DecodePParamsAlonzo(data []byte) (any, error) { + var ret alonzo.AlonzoProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkAlonzo(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + maryPParams, ok := prevPParams.(mary.MaryProtocolParameters) + if !ok { + return nil, fmt.Errorf("previous PParams (%T) are not expected type", prevPParams) + } + ret := alonzo.UpgradePParams(maryPParams) + alonzoGenesis, err := nodeConfig.AlonzoGenesis() + if err != nil { + return nil, err + } + ret.UpdateFromGenesis(alonzoGenesis) + return ret, nil +} diff --git a/state/eras/babbage.go b/state/eras/babbage.go new file mode 100644 index 0000000..4af5351 --- /dev/null +++ b/state/eras/babbage.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 eras + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/alonzo" + "github.com/blinklabs-io/gouroboros/ledger/babbage" + "github.com/blinklabs-io/node/config/cardano" +) + +var BabbageEraDesc = EraDesc{ + Id: babbage.EraIdBabbage, + Name: babbage.EraNameBabbage, + DecodePParamsFunc: DecodePParamsBabbage, + HardForkFunc: HardForkBabbage, +} + +func DecodePParamsBabbage(data []byte) (any, error) { + var ret babbage.BabbageProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkBabbage(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + alonzoPParams, ok := prevPParams.(alonzo.AlonzoProtocolParameters) + if !ok { + return nil, fmt.Errorf("previous PParams (%T) are not expected type", prevPParams) + } + ret := babbage.UpgradePParams(alonzoPParams) + return ret, nil +} diff --git a/state/eras/byron.go b/state/eras/byron.go new file mode 100644 index 0000000..a735515 --- /dev/null +++ b/state/eras/byron.go @@ -0,0 +1,22 @@ +// Copyright 2024 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 eras + +import "github.com/blinklabs-io/gouroboros/ledger/byron" + +var ByronEraDesc = EraDesc{ + Id: byron.EraIdByron, + Name: byron.EraNameByron, +} diff --git a/state/eras/conway.go b/state/eras/conway.go new file mode 100644 index 0000000..7a5c3f4 --- /dev/null +++ b/state/eras/conway.go @@ -0,0 +1,53 @@ +// Copyright 2024 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 eras + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/babbage" + "github.com/blinklabs-io/gouroboros/ledger/conway" + "github.com/blinklabs-io/node/config/cardano" +) + +var ConwayEraDesc = EraDesc{ + Id: conway.EraIdConway, + Name: conway.EraNameConway, + DecodePParamsFunc: DecodePParamsConway, + HardForkFunc: HardForkConway, +} + +func DecodePParamsConway(data []byte) (any, error) { + var ret conway.ConwayProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkConway(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + babbagePParams, ok := prevPParams.(babbage.BabbageProtocolParameters) + if !ok { + return nil, fmt.Errorf("previous PParams (%T) are not expected type", prevPParams) + } + ret := conway.UpgradePParams(babbagePParams) + conwayGenesis, err := nodeConfig.ConwayGenesis() + if err != nil { + return nil, err + } + ret.UpdateFromGenesis(conwayGenesis) + return ret, nil +} diff --git a/state/eras/eras.go b/state/eras/eras.go new file mode 100644 index 0000000..41b73f2 --- /dev/null +++ b/state/eras/eras.go @@ -0,0 +1,34 @@ +// Copyright 2024 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 eras + +import "github.com/blinklabs-io/node/config/cardano" + +type EraDesc struct { + Id uint + Name string + DecodePParamsFunc func([]byte) (any, error) + HardForkFunc func(*cardano.CardanoNodeConfig, any) (any, error) +} + +var Eras = []EraDesc{ + ByronEraDesc, + ShelleyEraDesc, + AllegraEraDesc, + MaryEraDesc, + AlonzoEraDesc, + BabbageEraDesc, + ConwayEraDesc, +} diff --git a/state/eras/mary.go b/state/eras/mary.go new file mode 100644 index 0000000..8a6875c --- /dev/null +++ b/state/eras/mary.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 eras + +import ( + "fmt" + + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/allegra" + "github.com/blinklabs-io/gouroboros/ledger/mary" + "github.com/blinklabs-io/node/config/cardano" +) + +var MaryEraDesc = EraDesc{ + Id: mary.EraIdMary, + Name: mary.EraNameMary, + DecodePParamsFunc: DecodePParamsMary, + HardForkFunc: HardForkMary, +} + +func DecodePParamsMary(data []byte) (any, error) { + var ret mary.MaryProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkMary(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + allegraPParams, ok := prevPParams.(allegra.AllegraProtocolParameters) + if !ok { + return nil, fmt.Errorf("previous PParams (%T) are not expected type", prevPParams) + } + ret := mary.UpgradePParams(allegraPParams) + return ret, nil +} diff --git a/state/eras/shelley.go b/state/eras/shelley.go new file mode 100644 index 0000000..2a9fc1d --- /dev/null +++ b/state/eras/shelley.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 eras + +import ( + "github.com/blinklabs-io/gouroboros/cbor" + "github.com/blinklabs-io/gouroboros/ledger/shelley" + "github.com/blinklabs-io/node/config/cardano" +) + +var ShelleyEraDesc = EraDesc{ + Id: shelley.EraIdShelley, + Name: shelley.EraNameShelley, + DecodePParamsFunc: DecodePParamsShelley, + HardForkFunc: HardForkShelley, +} + +func DecodePParamsShelley(data []byte) (any, error) { + var ret shelley.ShelleyProtocolParameters + if _, err := cbor.Decode(data, &ret); err != nil { + return nil, err + } + return ret, nil +} + +func HardForkShelley(nodeConfig *cardano.CardanoNodeConfig, prevPParams any) (any, error) { + // There's no Byron protocol parameters to upgrade from, so this is mostly + // a dummy call for consistency + ret := shelley.UpgradePParams(nil) + shelleyGenesis, err := nodeConfig.ShelleyGenesis() + if err != nil { + return nil, err + } + ret.UpdateFromGenesis(shelleyGenesis) + return ret, nil +} diff --git a/state/models/era.go b/state/models/era.go new file mode 100644 index 0000000..b204ae7 --- /dev/null +++ b/state/models/era.go @@ -0,0 +1,48 @@ +// Copyright 2024 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 models + +import "github.com/blinklabs-io/node/database" + +type Era struct { + ID uint `gorm:"primarykey"` + EraId uint + StartEpoch uint +} + +func (Era) TableName() string { + return "era" +} + +func EraLatest(db database.Database) (Era, error) { + var ret Era + txn := db.Transaction(false) + err := txn.Do(func(txn *database.Txn) error { + var err error + ret, err = EraLatestTxn(txn) + return err + }) + return ret, err +} + +func EraLatestTxn(txn *database.Txn) (Era, error) { + var ret Era + result := txn.Metadata().Order("era_id DESC"). + First(&ret) + if result.Error != nil { + return ret, result.Error + } + return ret, nil +} diff --git a/state/models/models.go b/state/models/models.go index 435ccaf..edb2c72 100644 --- a/state/models/models.go +++ b/state/models/models.go @@ -18,6 +18,8 @@ package models var MigrateModels = []any{ &Block{}, &Epoch{}, + &Era{}, + &PParams{}, &PParamUpdate{}, &Utxo{}, } diff --git a/state/models/pparams.go b/state/models/pparams.go new file mode 100644 index 0000000..7dde77e --- /dev/null +++ b/state/models/pparams.go @@ -0,0 +1,27 @@ +// Copyright 2024 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 models + +type PParams struct { + ID uint `gorm:"primarykey"` + AddedSlot uint64 + Epoch uint + EraId uint + Cbor []byte +} + +func (PParams) TableName() string { + return "pparams" +} diff --git a/state/state.go b/state/state.go index 630a2fd..4fa2128 100644 --- a/state/state.go +++ b/state/state.go @@ -22,15 +22,18 @@ import ( "sync" "time" + "github.com/blinklabs-io/node/config/cardano" "github.com/blinklabs-io/node/database" "github.com/blinklabs-io/node/event" + "github.com/blinklabs-io/node/state/eras" "github.com/blinklabs-io/node/state/models" - "gorm.io/gorm" + "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger" "github.com/blinklabs-io/gouroboros/ledger/byron" ochainsync "github.com/blinklabs-io/gouroboros/protocol/chainsync" ocommon "github.com/blinklabs-io/gouroboros/protocol/common" + "gorm.io/gorm" ) const ( @@ -38,40 +41,40 @@ const ( cleanupConsumedUtxosSlotWindow = 50000 // TODO: calculate this from params ) +type LedgerStateConfig struct { + Logger *slog.Logger + DataDir string + EventBus *event.EventBus + CardanoNodeConfig *cardano.CardanoNodeConfig +} + type LedgerState struct { sync.RWMutex - logger *slog.Logger - dataDir string + config LedgerStateConfig db database.Database - eventBus *event.EventBus timerCleanupConsumedUtxos *time.Timer - // TODO: move this into the DB - currentEra uint8 + currentPParams any + currentEpoch models.Epoch + currentEraId uint8 } -func NewLedgerState( - dataDir string, - eventBus *event.EventBus, - logger *slog.Logger, -) (*LedgerState, error) { +func NewLedgerState(cfg LedgerStateConfig) (*LedgerState, error) { ls := &LedgerState{ - dataDir: dataDir, - logger: logger, - eventBus: eventBus, + config: cfg, } - if logger == nil { + if cfg.Logger == nil { // Create logger to throw away logs // We do this so we don't have to add guards around every log operation - ls.logger = slog.New(slog.NewJSONHandler(io.Discard, nil)) + cfg.Logger = slog.New(slog.NewJSONHandler(io.Discard, nil)) } - if dataDir == "" { - db, err := database.NewInMemory(ls.logger) + if cfg.DataDir == "" { + db, err := database.NewInMemory(ls.config.Logger) if err != nil { return nil, err } ls.db = db } else { - db, err := database.NewPersistent(dataDir, ls.logger) + db, err := database.NewPersistent(cfg.DataDir, cfg.Logger) if err != nil { return nil, err } @@ -84,10 +87,22 @@ func NewLedgerState( } } // Setup event handlers - ls.eventBus.SubscribeFunc(ChainsyncEventType, ls.handleEventChainSync) + ls.config.EventBus.SubscribeFunc(ChainsyncEventType, ls.handleEventChainSync) // Schedule periodic process to purge consumed UTxOs outside of the rollback window ls.scheduleCleanupConsumedUtxos() // TODO: schedule process to scan/clean blob DB for keys that don't have a corresponding metadata DB entry + // Load current era from DB + if err := ls.loadEra(); err != nil { + return nil, err + } + // Load current epoch from DB + if err := ls.loadEpoch(); err != nil { + return nil, err + } + // Load current protocol parameters from DB + if err := ls.loadPParams(); err != nil { + return nil, err + } return ls, nil } @@ -110,7 +125,7 @@ func (ls *LedgerState) scheduleCleanupConsumedUtxos() { // Get the current tip, since we're querying by slot tip, err := ls.Tip() if err != nil { - ls.logger.Error( + ls.config.Logger.Error( "failed to get tip", "component", "ledger", "error", err, @@ -148,7 +163,7 @@ func (ls *LedgerState) scheduleCleanupConsumedUtxos() { return nil }) if err != nil { - ls.logger.Error( + ls.config.Logger.Error( "failed to update utxos", "component", "ledger", "error", err, @@ -170,7 +185,7 @@ func (ls *LedgerState) handleEventChainSync(evt event.Event) { if e.Rollback { if err := ls.handleEventChainSyncRollback(e); err != nil { // TODO: actually handle this error - ls.logger.Error( + ls.config.Logger.Error( "failed to handle rollback", "component", "ledger", "error", err, @@ -180,7 +195,7 @@ func (ls *LedgerState) handleEventChainSync(evt event.Event) { } else if e.Block != nil { if err := ls.handleEventChainSyncBlock(e); err != nil { // TODO: actually handle this error - ls.logger.Error( + ls.config.Logger.Error( "failed to handle block", "component", "ledger", "error", err, @@ -190,7 +205,7 @@ func (ls *LedgerState) handleEventChainSync(evt event.Event) { } else if e.BlockHeader != nil { if err := ls.handleEventChainSyncBlockHeader(e); err != nil { // TODO: actually handle this error - ls.logger.Error( + ls.config.Logger.Error( fmt.Sprintf("ledger: failed to handle block header: %s", err), ) return @@ -245,7 +260,7 @@ func (ls *LedgerState) handleEventChainSyncRollback(e ChainsyncEvent) error { return err } // Generate event - ls.eventBus.Publish( + ls.config.EventBus.Publish( ChainRollbackEventType, event.NewEvent( ChainRollbackEventType, @@ -254,7 +269,7 @@ func (ls *LedgerState) handleEventChainSyncRollback(e ChainsyncEvent) error { }, ), ) - ls.logger.Info( + ls.config.Logger.Info( fmt.Sprintf( "chain rolled back, new tip: %x at slot %d", e.Point.Hash, @@ -281,47 +296,43 @@ func (ls *LedgerState) handleEventChainSyncBlock(e ChainsyncEvent) error { Type: e.Type, Cbor: e.Block.Cbor(), } - // TODO: track this using protocol params and hard forks - ls.currentEra = e.Block.Era().Id // Start a transaction txn := ls.db.Transaction(true) err := txn.Do(func(txn *database.Txn) error { + // TODO: move this somewhere else // Check for epoch change - latestEpoch, err := models.EpochLatestTxn(txn) - if err != nil { - if err != gorm.ErrRecordNotFound { - return err - } + if ls.currentEpoch.ID == 0 { // Create initial epoch - latestEpoch = models.Epoch{ + newEpoch := models.Epoch{ EpochId: 0, - EraId: ls.currentEra, + EraId: ls.currentEraId, StartSlot: 0, SlotLengthInSeconds: 20, LengthInSlots: 21600, } - if result := txn.Metadata().Create(&latestEpoch); result.Error != nil { + if result := txn.Metadata().Create(&newEpoch); result.Error != nil { return result.Error } - ls.logger.Debug( + ls.currentEpoch = newEpoch + ls.config.Logger.Debug( "added initial epoch to DB", - "epoch", fmt.Sprintf("%+v", latestEpoch), + "epoch", fmt.Sprintf("%+v", newEpoch), "component", "ledger", ) } - if e.Point.Slot > latestEpoch.StartSlot+uint64( - latestEpoch.LengthInSlots, + if e.Point.Slot > ls.currentEpoch.StartSlot+uint64( + ls.currentEpoch.LengthInSlots, ) { // Create next epoch record newEpoch := models.Epoch{ - EpochId: latestEpoch.EpochId + 1, - EraId: ls.currentEra, - StartSlot: latestEpoch.StartSlot + uint64( - latestEpoch.LengthInSlots, + EpochId: ls.currentEpoch.EpochId + 1, + EraId: e.Block.Era().Id, + StartSlot: ls.currentEpoch.StartSlot + uint64( + ls.currentEpoch.LengthInSlots, ), } // TODO: replace with protocol param lookups - if ls.currentEra == byron.EraIdByron { + if ls.currentEraId == byron.EraIdByron { newEpoch.SlotLengthInSeconds = 20 newEpoch.LengthInSlots = 21600 } else { @@ -331,12 +342,60 @@ func (ls *LedgerState) handleEventChainSyncBlock(e ChainsyncEvent) error { if result := txn.Metadata().Create(&newEpoch); result.Error != nil { return result.Error } - ls.logger.Debug( + ls.currentEpoch = newEpoch + ls.config.Logger.Debug( "added next epoch to DB", "epoch", fmt.Sprintf("%+v", newEpoch), "component", "ledger", ) } + // TODO: move this somewhere else + // TODO: track this using protocol params and hard forks + // Check for era rollover + if e.Block.Era().Id != ls.currentEraId { + targetEraId := e.Block.Era().Id + // Transition through every era between the current and the target era + for nextEraId := ls.currentEraId + 1; nextEraId <= targetEraId; nextEraId++ { + nextEra := eras.Eras[nextEraId] + if nextEra.HardForkFunc != nil { + // Perform hard fork + // This generally means upgrading pparams from previous era + newPParams, err := nextEra.HardForkFunc(ls.config.CardanoNodeConfig, ls.currentPParams) + if err != nil { + return err + } + ls.currentPParams = newPParams + ls.config.Logger.Debug( + "updated protocol params", + "pparams", + fmt.Sprintf("%#v", ls.currentPParams), + ) + // Write pparams update to DB + pparamsCbor, err := cbor.Encode(&ls.currentPParams) + if err != nil { + return err + } + tmpPParams := models.PParams{ + AddedSlot: e.Point.Slot, + Epoch: ls.currentEpoch.EpochId, + EraId: uint(nextEraId), + Cbor: pparamsCbor, + } + if result := txn.Metadata().Create(&tmpPParams); result.Error != nil { + return result.Error + } + } + ls.currentEraId = nextEraId + // Add entry for era to DB + tmpEra := models.Era{ + EraId: uint(nextEraId), + StartEpoch: ls.currentEpoch.EpochId, + } + if result := txn.Metadata().Create(&tmpEra); result.Error != nil { + return result.Error + } + } + } // Add block to database if err := ls.addBlock(txn, tmpBlock); err != nil { return fmt.Errorf("add block: %w", err) @@ -386,7 +445,7 @@ func (ls *LedgerState) handleEventChainSyncBlock(e ChainsyncEvent) error { return err } // Generate event - ls.eventBus.Publish( + ls.config.EventBus.Publish( ChainBlockEventType, event.NewEvent( ChainBlockEventType, @@ -396,7 +455,7 @@ func (ls *LedgerState) handleEventChainSyncBlock(e ChainsyncEvent) error { }, ), ) - ls.logger.Info( + ls.config.Logger.Info( fmt.Sprintf( "chain extended, new tip: %s at slot %d", e.Block.Hash(), @@ -477,6 +536,47 @@ func (ls *LedgerState) removeBlock( return nil } +func (ls *LedgerState) loadPParams() error { + var tmpPParams models.PParams + result := ls.db.Metadata().Order("id DESC").First(&tmpPParams) + if result.Error != nil { + if result.Error == gorm.ErrRecordNotFound { + return nil + } + return result.Error + } + currentPParams, err := eras.Eras[ls.currentEraId].DecodePParamsFunc(tmpPParams.Cbor) + if err != nil { + return err + } + ls.currentPParams = currentPParams + return nil +} + +func (ls *LedgerState) loadEpoch() error { + tmpEpoch, err := models.EpochLatest(ls.db) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return err + } + ls.currentEpoch = tmpEpoch + return nil +} + +func (ls *LedgerState) loadEra() error { + tmpEra, err := models.EraLatest(ls.db) + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil + } + return err + } + ls.currentEraId = uint8(tmpEra.EraId) + return nil +} + func (ls *LedgerState) GetBlock(point ocommon.Point) (*models.Block, error) { ret, err := models.BlockByPoint(ls.db, point) if err != nil { diff --git a/topology/topology.go b/topology/topology.go index 3f3aa26..0a2ed2a 100644 --- a/topology/topology.go +++ b/topology/topology.go @@ -26,7 +26,7 @@ type TopologyConfig struct { LocalRoots []TopologyConfigP2PLocalRoot `json:"localRoots"` PublicRoots []TopologyConfigP2PPublicRoot `json:"publicRoots"` BootstrapPeers []TopologyConfigP2PBootstrapPeer `json:"bootstrapPeers"` - UseLedgerAfterSlot uint64 `json:"useLedgerAfterSlot"` + UseLedgerAfterSlot int64 `json:"useLedgerAfterSlot"` } type TopologyConfigLegacyProducer struct {