Skip to content
This repository has been archived by the owner on Jan 24, 2025. It is now read-only.

Add optional check when starting engine about correctness of last commitment and ledger state #803

Merged
merged 18 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
polinikita marked this conversation as resolved.
Show resolved Hide resolved
}, 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
Loading