Skip to content

Commit

Permalink
Merge pull request #803 from iotaledger/feat/check-last-commitment-up…
Browse files Browse the repository at this point in the history
…on-startup

Add optional check when starting engine about correctness of last commitment and ledger state
  • Loading branch information
piotrm50 authored Mar 14, 2024
2 parents 0be95a4 + eb57133 commit f5b974b
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 11 deletions.
1 change: 1 addition & 0 deletions components/protocol/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ func provide(c *dig.Container) error {
),
),
protocol.WithSnapshotPath(ParamsProtocol.Snapshot.Path),
protocol.WithCommitmentCheck(ParamsProtocol.CommitmentCheck),
protocol.WithMaxAllowedWallClockDrift(ParamsProtocol.Filter.MaxAllowedClockDrift),
protocol.WithPreSolidFilterProvider(
presolidblockfilter.NewProvider(),
Expand Down
2 changes: 2 additions & 0 deletions components/protocol/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type ParametersProtocol struct {
Depth int `default:"5" usage:"defines how many slot diffs are stored in the snapshot, starting from the full ledgerstate"`
}

CommitmentCheck bool `default:"true" usage:"specifies whether commitment and ledger checks should be enabled"`

Filter struct {
// MaxAllowedClockDrift defines the maximum drift our wall clock can have to future blocks being received from the network.
MaxAllowedClockDrift time.Duration `default:"5s" usage:"the maximum drift our wall clock can have to future blocks being received from the network"`
Expand Down
23 changes: 21 additions & 2 deletions pkg/protocol/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type Engine struct {
optsSnapshotPath string
optsEntryPointsDepth int
optsSnapshotDepth int
optsCheckCommitment bool
optsBlockRequester []options.Option[eventticker.EventTicker[iotago.SlotIndex, iotago.BlockID]]

*module.ReactiveModule
Expand Down Expand Up @@ -129,8 +130,9 @@ func New(
LatestCommitment: reactive.NewVariable[*model.Commitment](),
Workers: workers,

optsSnapshotPath: "snapshot.bin",
optsSnapshotDepth: 5,
optsSnapshotPath: "snapshot.bin",
optsSnapshotDepth: 5,
optsCheckCommitment: true,
}, opts, func(e *Engine) {
e.ReactiveModule = e.initReactiveModule(logger)

Expand Down Expand Up @@ -230,6 +232,13 @@ func New(
e.Reset()
}

// Check consistency of commitment and ledger state in the storage
if e.optsCheckCommitment {
if err := e.Storage.CheckCorrectnessCommitmentLedgerState(); err != nil {
panic(ierrors.Wrap(err, "commitment or ledger state are incorrect"))
}
}

e.Initialized.Trigger()

e.LogTrace("initialized", "settings", e.Storage.Settings().String())
Expand Down Expand Up @@ -382,6 +391,8 @@ func (e *Engine) ImportContents(reader io.ReadSeeker) (err error) {
return ierrors.Wrap(err, "failed to import attestation state")
} else if err = e.UpgradeOrchestrator.Import(reader); err != nil {
return ierrors.Wrap(err, "failed to import upgrade orchestrator")
} else if err = e.Storage.ImportRoots(reader, e.Storage.Settings().LatestCommitment()); err != nil {
return ierrors.Wrap(err, "failed to import roots")
}

return
Expand All @@ -408,6 +419,8 @@ func (e *Engine) Export(writer io.WriteSeeker, targetSlot iotago.SlotIndex) (err
return ierrors.Wrap(err, "failed to export attestation state")
} else if err = e.UpgradeOrchestrator.Export(writer, targetSlot); err != nil {
return ierrors.Wrap(err, "failed to export upgrade orchestrator")
} else if err = e.Storage.ExportRoots(writer, targetCommitment.Commitment()); err != nil {
return ierrors.Wrap(err, "failed to export roots")
}

return
Expand Down Expand Up @@ -622,6 +635,12 @@ func WithSnapshotPath(snapshotPath string) options.Option[Engine] {
}
}

func WithCommitmentCheck(checkCommitment bool) options.Option[Engine] {
return func(e *Engine) {
e.optsCheckCommitment = checkCommitment
}
}

func WithEntryPointsDepth(entryPointsDepth int) options.Option[Engine] {
return func(engine *Engine) {
engine.optsEntryPointsDepth = entryPointsDepth
Expand Down
12 changes: 6 additions & 6 deletions pkg/protocol/engines.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (e *Engines) ForkAtSlot(slot iotago.SlotIndex) (*engine.Engine, error) {
}

// loadMainEngine loads the main engine from disk or creates a new one if no engine exists.
func (e *Engines) loadMainEngine(snapshotPath string) (*engine.Engine, error) {
func (e *Engines) loadMainEngine(snapshotPath string, commitmentCheck bool) (*engine.Engine, error) {
info := &engineInfo{}
if err := ioutils.ReadJSONFromFile(e.infoFilePath(), info); err != nil && !ierrors.Is(err, os.ErrNotExist) {
return nil, ierrors.Errorf("unable to read engine info file: %w", err)
Expand All @@ -157,12 +157,12 @@ func (e *Engines) loadMainEngine(snapshotPath string) (*engine.Engine, error) {
// load previous engine as main engine if it exists.
if len(info.Name) > 0 {
if exists, isDirectory, err := ioutils.PathExists(e.directory.Path(info.Name)); err == nil && exists && isDirectory {
return e.loadEngineInstanceFromSnapshot(info.Name, snapshotPath)
return e.loadEngineInstanceFromSnapshot(info.Name, snapshotPath, commitmentCheck)
}
}

// load new engine if no previous engine exists.
return e.loadEngineInstanceFromSnapshot(lo.PanicOnErr(uuid.NewUUID()).String(), snapshotPath)
return e.loadEngineInstanceFromSnapshot(lo.PanicOnErr(uuid.NewUUID()).String(), snapshotPath, commitmentCheck)
})

// cleanup candidates
Expand Down Expand Up @@ -199,12 +199,12 @@ func (e *Engines) infoFilePath() string {
}

// loadEngineInstanceFromSnapshot loads an engine instance from a snapshot.
func (e *Engines) loadEngineInstanceFromSnapshot(engineAlias string, snapshotPath string) *engine.Engine {
func (e *Engines) loadEngineInstanceFromSnapshot(engineAlias string, snapshotPath string, commitmentCheck bool) *engine.Engine {
errorHandler := func(err error) {
e.protocol.LogError("engine error", "err", err, "name", engineAlias[0:8])
}

return e.loadEngineInstanceWithStorage(engineAlias, storage.Create(e.Logger, e.directory.Path(engineAlias), DatabaseVersion, errorHandler, e.protocol.Options.StorageOptions...), engine.WithSnapshotPath(snapshotPath))
return e.loadEngineInstanceWithStorage(engineAlias, storage.Create(e.Logger, e.directory.Path(engineAlias), DatabaseVersion, errorHandler, e.protocol.Options.StorageOptions...), engine.WithSnapshotPath(snapshotPath), engine.WithCommitmentCheck(commitmentCheck))
}

// loadEngineInstanceWithStorage loads an engine instance with the given storage.
Expand Down Expand Up @@ -268,7 +268,7 @@ func (e *Engines) injectEngineInstances() (shutdown func()) {

if newEngine, err := func() (*engine.Engine, error) {
if e.Main.Get() == nil {
return e.loadMainEngine(e.protocol.Options.SnapshotPath)
return e.loadMainEngine(e.protocol.Options.SnapshotPath, e.protocol.Options.CommitmentCheck)
}

return e.ForkAtSlot(chain.ForkingPoint.Get().Slot() - 1)
Expand Down
10 changes: 10 additions & 0 deletions pkg/protocol/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type Options struct {
// SnapshotPath is the path to the snapshot file that should be used to initialize the protocol.
SnapshotPath string

// CommitmentCheck is an opt flag that allows engines check correctness of commitment and ledger state upon startup.
CommitmentCheck bool

// MaxAllowedWallClockDrift specifies how far in the future are blocks allowed to be ahead of our own wall clock (defaults to 0 seconds).
MaxAllowedWallClockDrift time.Duration

Expand Down Expand Up @@ -162,6 +165,13 @@ func WithSnapshotPath(snapshot string) options.Option[Protocol] {
}
}

// WithCommitmentCheck is an option for the Protocol that allows to check the commitment and ledger state upon startup.
func WithCommitmentCheck(commitmentCheck bool) options.Option[Protocol] {
return func(p *Protocol) {
p.Options.CommitmentCheck = commitmentCheck
}
}

// WithMaxAllowedWallClockDrift specifies how far in the future are blocks allowed to be ahead of our own wall clock (defaults to 0 seconds).
func WithMaxAllowedWallClockDrift(d time.Duration) options.Option[Protocol] {
return func(p *Protocol) {
Expand Down
59 changes: 59 additions & 0 deletions pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,62 @@ func (s *Storage) Flush() {
s.permanent.Flush()
s.prunable.Flush()
}

// Checks the correctness of the latest commitment.
// Additionally, for non-genesis slots it checks whether the ledger state corresponds to the state root.
func (s *Storage) CheckCorrectnessCommitmentLedgerState() error {

// Get the latest commitment
latestCommitment := s.Settings().LatestCommitment()

latestCommitmentID := latestCommitment.ID()
latestCommittedSlotIndex := latestCommitment.Slot()

// Do the ledger state check only for non-genesis slots.
// TODO: once the genesis slot provides the roots, change this
if latestCommittedSlotIndex > s.Settings().APIProvider().CommittedAPI().ProtocolParameters().GenesisSlot() {
// Get the state root in the permanent storage (that corresponds to the last commitment)
latestStateRoot := s.Ledger().StateTreeRoot()

// Load root storage from prunable storage
rootsStorage, err := s.Roots(latestCommittedSlotIndex)
if err != nil {
return ierrors.Wrap(err, "failed to load roots storage")
}

// Load roots from prunable storage that correspond to the last committed slot index and commitment
roots, exists, err := rootsStorage.Load(latestCommitmentID)
if err != nil {
return ierrors.Wrap(err, "failed to load roots from prunable storage")
} else if !exists {
return ierrors.Wrap(err, "roots not found")
}

// Check the correctness of stored state root and the state root computed from the stored ledger state
if roots.StateRoot != latestStateRoot {
return ierrors.Wrap(err, "computed state root from storage does not correspond to stored state root")
}

if roots.ID() != latestCommitment.RootsID() {
return ierrors.Wrap(err, "root from prunable storage does not correspond to root from commitment")
}

}

// Verify the correctness of the slot commitment
computeCurrentCommitment := iotago.NewCommitment(
latestCommitment.Commitment().ProtocolVersion,
latestCommittedSlotIndex,
latestCommitment.PreviousCommitmentID(),
latestCommitment.RootsID(),
latestCommitment.CumulativeWeight(),
latestCommitment.ReferenceManaCost(),
)
computeCurrentCommitmentID := computeCurrentCommitment.MustID()

if computeCurrentCommitmentID != latestCommitmentID {
return ierrors.New("Computed commitment ID is different from the stored one")
}

return nil
}
138 changes: 138 additions & 0 deletions pkg/storage/storage_prunable.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package storage

import (
"io"

"github.com/iotaledger/hive.go/ierrors"
"github.com/iotaledger/hive.go/kvstore"
"github.com/iotaledger/hive.go/serializer/v2/stream"
"github.com/iotaledger/iota-core/pkg/core/account"
"github.com/iotaledger/iota-core/pkg/model"
"github.com/iotaledger/iota-core/pkg/storage/prunable/epochstore"
Expand Down Expand Up @@ -109,6 +112,141 @@ func (s *Storage) Roots(slot iotago.SlotIndex) (*slotstore.Store[iotago.Commitme
return s.prunable.Roots(slot)
}

func (s *Storage) ExportRoots(writer io.WriteSeeker, targetCommitment *iotago.Commitment) error {

slotIndex := targetCommitment.Slot

if slotIndex <= s.Settings().APIProvider().CommittedAPI().ProtocolParameters().GenesisSlot() {
return nil
}

commitmentID, err := targetCommitment.ID()

if err != nil {
return ierrors.Wrap(err, "can not retrieve commitment id")
}
// Load root storage from prunable storage
rootsStorage, errRoots := s.Roots(slotIndex)

if errRoots != nil {
return ierrors.Wrap(err, "failed to load roots storage")
}

roots, exists, errLoad := rootsStorage.Load(commitmentID)
if errLoad != nil {
return ierrors.Wrap(err, "failed to load roots from prunable storage")
} else if !exists {
return ierrors.Wrap(err, "roots not found")
}

if errWrite := stream.Write(writer, roots.AccountRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write account root bytes")
}
if errWrite := stream.Write(writer, roots.AttestationsRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write attestation root bytes")
}
if errWrite := stream.Write(writer, roots.CommitteeRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write committee root bytes")
}
if errWrite := stream.Write(writer, roots.ProtocolParametersHash); errWrite != nil {
return ierrors.Wrapf(err, "failed to write protocol parameters hash root bytes")
}
if errWrite := stream.Write(writer, roots.RewardsRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write rewards root bytes")
}
if errWrite := stream.Write(writer, roots.StateMutationRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write state mutation root bytes")
}
if errWrite := stream.Write(writer, roots.StateRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write state root bytes")
}
if errWrite := stream.Write(writer, roots.TangleRoot); errWrite != nil {
return ierrors.Wrapf(err, "failed to write tangle root bytes")
}

return err
}

func (s *Storage) ImportRoots(reader io.ReadSeeker, targetCommitment *model.Commitment) error {

slotIndex := targetCommitment.Commitment().Slot

if slotIndex <= s.Settings().APIProvider().CommittedAPI().ProtocolParameters().GenesisSlot() {
return nil
}

accountRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve account root")
}

attestationRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve attestation root")
}

committeeRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve committee root")
}

protocolParametersHash, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve protocol parameters hash")
}

rewardsRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve rewards root")
}

stateMutationRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve state mutation root")
}

stateRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve state root")
}

tangleRoot, err := stream.Read[iotago.Identifier](reader)
if err != nil {
return ierrors.Wrap(err, "can not retrieve tangle root")
}

commitmentID, err := targetCommitment.Commitment().ID()

if err != nil {
return ierrors.Wrap(err, "can not retrieve commitment id")
}
// Load root storage from prunable storage
rootsStorage, errRoots := s.Roots(slotIndex)

if errRoots != nil {
return ierrors.Wrap(err, "failed to load roots storage")
}

roots := iotago.NewRoots(
tangleRoot,
stateMutationRoot,
attestationRoot,
stateRoot,
accountRoot,
committeeRoot,
rewardsRoot,
protocolParametersHash,
)

errStore := rootsStorage.Store(commitmentID, roots)
if errStore != nil {
return ierrors.Wrap(err, "unable to store roots in storage")
}

return nil

}

func (s *Storage) BlockMetadata(slot iotago.SlotIndex) (*slotstore.BlockMetadataStore, error) {
if err := s.permanent.Settings().AdvanceLatestStoredSlot(slot); err != nil {
return nil, ierrors.Wrap(err, "failed to advance latest stored slot when accessing block metadata")
Expand Down
Loading

0 comments on commit f5b974b

Please sign in to comment.