diff --git a/block/block.go b/block/block.go index f0fcd8ad0..2b92d1fdf 100644 --- a/block/block.go +++ b/block/block.go @@ -92,6 +92,11 @@ func (m *Manager) applyBlock(block *types.Block, commit *types.Commit, blockMeta return fmt.Errorf("save block responses: %w", err) } + _, err = m.Store.SaveBlockSource(block.Header.Height, blockMetaData.Source.String(), nil) + if err != nil { + return fmt.Errorf("save block responses: %w", err) + } + // Commit block to app appHash, retainHeight, err = m.Executor.Commit(m.State, block, responses) if err != nil { diff --git a/block/manager.go b/block/manager.go index 388171479..4a89feef4 100644 --- a/block/manager.go +++ b/block/manager.go @@ -30,6 +30,8 @@ import ( "github.com/dymensionxyz/dymint/p2p" "github.com/dymensionxyz/dymint/settlement" "github.com/dymensionxyz/dymint/types" + + uchannel "github.com/dymensionxyz/dymint/utils/channel" ) // Manager is responsible for aggregating transactions into blocks. @@ -67,9 +69,8 @@ type Manager struct { // Protect against processing two blocks at once when there are two routines handling incoming gossiped blocks, // and incoming DA blocks, respectively. retrieverMu sync.Mutex - // Protect against syncing twice from DA in case new batch is posted but it did not finish to sync yet. - syncFromDaMu sync.Mutex - Retriever da.BatchRetriever + + Retriever da.BatchRetriever // Cached blocks and commits for applying at future heights. The blocks may not be valid, because // we can only do full validation in sequential order. blockCache *Cache @@ -85,6 +86,14 @@ type Manager struct { // indexer indexerService *txindex.IndexerService + + syncingC chan struct{} + + validateC chan struct{} + + synced *uchannel.Nudger + + validator *StateUpdateValidator } // NewManager creates new block Manager. @@ -128,6 +137,9 @@ func NewManager( }, FraudHandler: nil, // TODO: create a default handler pruningC: make(chan int64, 10), // use of buffered channel to avoid blocking applyBlock thread. In case channel is full, pruning will be skipped, but the retain height can be pruned in the next iteration. + syncingC: make(chan struct{}, 1), + validateC: make(chan struct{}, 1), + synced: uchannel.NewNudger(), } err = m.LoadStateOnInit(store, genesis, logger) @@ -146,6 +158,8 @@ func NewManager( return nil, err } + m.validator = NewStateUpdateValidator(m.logger, m) + return m, nil } @@ -175,21 +189,28 @@ func (m *Manager) Start(ctx context.Context) error { return m.PruningLoop(ctx) }) + uerrors.ErrGroupGoLog(eg, m.logger, func() error { + return m.SyncLoop(ctx) + }) + + err = m.syncFromSettlement() + if err != nil { + return fmt.Errorf("sync block manager from settlement: %w", err) + } + /* ----------------------------- full node mode ----------------------------- */ if !isProposer { - // Full-nodes can sync from DA but it is not necessary to wait for it, since it can sync from P2P as well in parallel. - go func() { - err := m.syncFromSettlement() - if err != nil { - m.logger.Error("sync block manager from settlement", "err", err) - } - // DA Sync. Subscribe to SL next batch events - go uevent.MustSubscribe(ctx, m.Pubsub, "syncTargetLoop", settlement.EventQueryNewSettlementBatchAccepted, m.onNewStateUpdate, m.logger) - }() + + uerrors.ErrGroupGoLog(eg, m.logger, func() error { + return m.ValidateLoop(ctx) + }) + go uevent.MustSubscribe(ctx, m.Pubsub, "syncLoop", settlement.EventQueryNewSettlementBatchAccepted, m.onNewStateUpdate, m.logger) + go uevent.MustSubscribe(ctx, m.Pubsub, "validateLoop", settlement.EventQueryNewSettlementBatchFinalized, m.onNewStateUpdateFinalized, m.logger) // P2P Sync. Subscribe to P2P received blocks events go uevent.MustSubscribe(ctx, m.Pubsub, "applyGossipedBlocksLoop", p2p.EventQueryNewGossipedBlock, m.OnReceivedBlock, m.logger) go uevent.MustSubscribe(ctx, m.Pubsub, "applyBlockSyncBlocksLoop", p2p.EventQueryNewBlockSyncBlock, m.OnReceivedBlock, m.logger) + return nil } @@ -198,11 +219,10 @@ func (m *Manager) Start(ctx context.Context) error { go uevent.MustSubscribe(ctx, m.Pubsub, "updateSubmittedHeightLoop", settlement.EventQueryNewSettlementBatchAccepted, m.UpdateLastSubmittedHeight, m.logger) // Sequencer must wait till DA is synced to start submitting blobs - <-m.DAClient.Synced() - err = m.syncFromSettlement() - if err != nil { - return fmt.Errorf("sync block manager from settlement: %w", err) - } + m.DAClient.WaitForSyncing() + + // Sequencer must wait till node is synced till last submittedHeight, in case it is not + m.waitForSyncing() // check if sequencer in the middle of rotation nextSeqAddr, missing, err := m.MissingLastBatch() if err != nil { @@ -265,9 +285,10 @@ func (m *Manager) NextHeightToSubmit() uint64 { // syncFromSettlement enforces the node to be synced on initial run from SL and DA. func (m *Manager) syncFromSettlement() error { + // Update sequencers list from SL err := m.UpdateSequencerSetFromSL() if err != nil { - return fmt.Errorf("update bonded sequencer set: %w", err) + m.logger.Error("update bonded sequencer set", "error", err) } res, err := m.SLClient.GetLatestBatch() @@ -282,14 +303,21 @@ func (m *Manager) syncFromSettlement() error { // TODO: separate between fresh rollapp and non-registered rollapp return err } + m.LastSubmittedHeight.Store(res.EndHeight) - err = m.syncToTargetHeight(res.EndHeight) m.UpdateTargetHeight(res.EndHeight) + + // get the latest finalized height to know from where to start validating + err = m.UpdateFinalizedHeight() if err != nil { return err } - m.logger.Info("Synced.", "current height", m.State.Height(), "last submitted height", m.LastSubmittedHeight.Load()) + // try to sync to last state update submitted on startup + m.triggerStateUpdateSyncing() + // try to validate all pending state updates on startup + m.triggerStateUpdateValidation() + return nil } @@ -306,6 +334,24 @@ func (m *Manager) UpdateTargetHeight(h uint64) { } } +// UpdateFinalizedHeight retrieves the latest finalized batch and updates validation height with it +func (m *Manager) UpdateFinalizedHeight() error { + res, err := m.SLClient.GetLatestFinalizedBatch() + if err != nil && !errors.Is(err, gerrc.ErrNotFound) { + // The SL hasn't got any batches for this chain yet. + return fmt.Errorf("getting finalized height. err: %w", err) + } + if errors.Is(err, gerrc.ErrNotFound) { + // The SL hasn't got any batches for this chain yet. + m.logger.Info("No finalized batches for chain found in SL.") + m.State.SetLastValidatedHeight(0) + } else { + // update validation height with latest finalized height (it will be updated only of finalized height is higher) + m.State.SetLastValidatedHeight(res.EndHeight) + } + return nil +} + // ValidateConfigWithRollappParams checks the configuration params are consistent with the params in the dymint state (e.g. DA and version) func (m *Manager) ValidateConfigWithRollappParams() error { if version.Commit != m.State.RollappParams.Version { diff --git a/block/manager_test.go b/block/manager_test.go index e5d14962e..428c8d0e9 100644 --- a/block/manager_test.go +++ b/block/manager_test.go @@ -155,7 +155,6 @@ func TestProduceOnlyAfterSynced(t *testing.T) { require.NoError(t, err) manager, err := testutil.GetManagerWithProposerKey(chainId, testutil.GetManagerConfig(), proposerKey, nil, 1, 1, 0, proxyApp, nil) - //manager, err := testutil.GetManager(chainId, testutil.GetManagerConfig(), nil, 1, 1, 0, proxyApp, nil) require.NoError(t, err) require.NotNil(t, manager) @@ -387,7 +386,6 @@ func TestApplyLocalBlock_WithFraudCheck(t *testing.T) { }() <-ctx.Done() assert.Equal(t, batchs[1].EndHeight(), manager.LastSubmittedHeight.Load()) - mockExecutor.AssertExpectations(t) mockFraudHandler.AssertExpectations(t) } @@ -404,6 +402,7 @@ func TestRetrieveDaBatchesFailed(t *testing.T) { Client: da.Mock, Height: 1, } + err = manager.ProcessNextDABatch(daMetaData) t.Log(err) assert.ErrorIs(t, err, da.ErrBlobNotFound) @@ -746,6 +745,7 @@ func TestDAFetch(t *testing.T) { LastBlockHeight: int64(batch.EndHeight()), LastBlockAppHash: commitHash[:], }) + err := manager.ProcessNextDABatch(c.daMetaData) require.Equal(c.err, err) }) diff --git a/block/pruning.go b/block/pruning.go index d4ab18cc8..833263fed 100644 --- a/block/pruning.go +++ b/block/pruning.go @@ -7,10 +7,10 @@ import ( // PruneBlocks prune all block related data from dymint store up to (but not including) retainHeight. It returns the number of blocks pruned, used for testing. func (m *Manager) PruneBlocks(retainHeight uint64) (uint64, error) { - nextSubmissionHeight := m.NextHeightToSubmit() - if m.IsProposer() && nextSubmissionHeight < retainHeight { // do not delete anything that we might submit in future - m.logger.Debug("cannot prune blocks before they have been submitted. using height last submitted height for pruning", "retain_height", retainHeight, "height_to_submit", m.NextHeightToSubmit()) - retainHeight = nextSubmissionHeight + if m.IsProposer() { // do not delete anything that we might submit in future + retainHeight = max(m.NextHeightToSubmit(), retainHeight) + } else { // do not delete anything that is not validated yet + retainHeight = max(m.State.NextValidationHeight(), retainHeight) } // prune blocks from blocksync store diff --git a/block/retriever.go b/block/retriever.go index 405fde927..319a88140 100644 --- a/block/retriever.go +++ b/block/retriever.go @@ -1,94 +1,25 @@ package block import ( - "context" - "errors" "fmt" "github.com/dymensionxyz/gerr-cosmos/gerrc" "github.com/dymensionxyz/dymint/da" - "github.com/dymensionxyz/dymint/node/events" - "github.com/dymensionxyz/dymint/settlement" "github.com/dymensionxyz/dymint/types" - uevent "github.com/dymensionxyz/dymint/utils/event" - "github.com/tendermint/tendermint/libs/pubsub" ) -// onNewStateUpdate will try to sync to new height, if not already synced -func (m *Manager) onNewStateUpdate(event pubsub.Message) { - eventData, ok := event.Data().(*settlement.EventDataNewBatchAccepted) - if !ok { - m.logger.Error("onReceivedBatch", "err", "wrong event data received") - return - } - h := eventData.EndHeight - m.UpdateTargetHeight(h) - err := m.syncToTargetHeight(h) - if err != nil { - m.logger.Error("sync until target", "err", err) - } -} - -// syncToTargetHeight syncs blocks until the target height is reached. -// It fetches the batches from the settlement, gets the DA height and gets -// the actual blocks from the DA. -func (m *Manager) syncToTargetHeight(targetHeight uint64) error { - defer m.syncFromDaMu.Unlock() - m.syncFromDaMu.Lock() - for currH := m.State.NextHeight(); currH <= targetHeight; currH = m.State.NextHeight() { - // if we have the block locally, we don't need to fetch it from the DA - err := m.applyLocalBlock(currH) - if err == nil { - m.logger.Info("Synced from local", "store height", currH, "target height", targetHeight) - continue - } - if !errors.Is(err, gerrc.ErrNotFound) { - m.logger.Error("Apply local block", "err", err) - } - - err = m.syncFromDABatch() - if err != nil { - return fmt.Errorf("process next DA batch: %w", err) - } - - // if height havent been updated, we are stuck - if m.State.NextHeight() == currH { - return fmt.Errorf("stuck at height %d", currH) - } - m.logger.Info("Synced from DA", "store height", m.State.Height(), "target height", targetHeight) - - } - - err := m.attemptApplyCachedBlocks() - if err != nil { - uevent.MustPublish(context.TODO(), m.Pubsub, &events.DataHealthStatus{Error: err}, events.HealthStatusList) - m.logger.Error("Attempt apply cached blocks.", "err", err) - } - - return nil -} - func (m *Manager) syncFromDABatch() error { - // It's important that we query the state index before fetching the batch, rather - // than e.g. keep it and increment it, because we might be concurrently applying blocks - // and may require a higher index than expected. - res, err := m.SLClient.GetHeightState(m.State.NextHeight()) - if err != nil { - return fmt.Errorf("retrieve state: %w", err) - } - stateIndex := res.State.StateIndex - - settlementBatch, err := m.SLClient.GetBatchAtIndex(stateIndex) + settlementBatch, err := m.SLClient.GetBatchAtHeight(m.State.NextHeight()) if err != nil { return fmt.Errorf("retrieve batch: %w", err) } - m.logger.Info("Retrieved batch.", "state_index", stateIndex) + m.logger.Info("Retrieved batch.", "state_index", settlementBatch.StateIndex) // update the proposer when syncing from the settlement layer proposer := m.State.Sequencers.GetByAddress(settlementBatch.Batch.Sequencer) if proposer == nil { - return fmt.Errorf("proposer not found: batch: %d: %s", stateIndex, settlementBatch.Batch.Sequencer) + return fmt.Errorf("proposer not found: batch: %d: %s", settlementBatch.StateIndex, settlementBatch.Batch.Sequencer) } m.State.Sequencers.SetProposer(proposer) diff --git a/block/stateupdate_validator.go b/block/stateupdate_validator.go new file mode 100644 index 000000000..d176bff6a --- /dev/null +++ b/block/stateupdate_validator.go @@ -0,0 +1,163 @@ +package block + +import ( + "bytes" + "crypto/sha256" + "fmt" + + "github.com/dymensionxyz/dymint/da" + "github.com/dymensionxyz/dymint/settlement" + "github.com/dymensionxyz/dymint/types" +) + +// StateUpdateValidator is a validator for messages gossiped in the p2p network. +type StateUpdateValidator struct { + logger types.Logger + blockManager *Manager +} + +// NewValidator creates a new Validator. +func NewStateUpdateValidator(logger types.Logger, blockManager *Manager) *StateUpdateValidator { + return &StateUpdateValidator{ + logger: logger, + blockManager: blockManager, + } +} + +func (v *StateUpdateValidator) ValidateStateUpdate(batch *settlement.ResultRetrieveBatch) error { + v.logger.Debug("validating state update", "start height", batch.StartHeight, "end height", batch.EndHeight) + + err := v.validateDRS(batch.StartHeight, batch.EndHeight, batch.DRSVersion) + if err != nil { + return err + } + + var daBlocks []*types.Block + var p2pBlocks []*types.Block + + // load blocks for the batch height, either P2P or DA blocks + for height := batch.StartHeight; height <= batch.EndHeight; height++ { + source, err := v.blockManager.Store.LoadBlockSource(height) + if err != nil { + continue + } + block, err := v.blockManager.Store.LoadBlock(height) + if err != nil { + continue + } + if source == types.DA.String() { + daBlocks = append(daBlocks, block) + } else { + p2pBlocks = append(p2pBlocks, block) + } + } + + // if not all blocks are applied from DA, it is necessary to get all batch blocks from DA + numBlocks := batch.EndHeight - batch.StartHeight + 1 + if uint64(len(daBlocks)) != numBlocks { + daBlocks = []*types.Block{} + var daBatch da.ResultRetrieveBatch + for { + daBatch = v.blockManager.Retriever.RetrieveBatches(batch.MetaData.DA) + if daBatch.Code == da.StatusSuccess { + break + } + } + for _, batch := range daBatch.Batches { + daBlocks = append(daBlocks, batch.Blocks...) + } + } + + // validate DA blocks against the state update + err = v.ValidateDaBlocks(batch, daBlocks) + if err != nil { + return err + } + + // compare the batch blocks with the blocks applied from P2P + err = v.ValidateP2PBlocks(daBlocks, p2pBlocks) + if err != nil { + return err + } + + // update the last validated height to the batch last block height + v.blockManager.State.SetLastValidatedHeight(batch.EndHeight) + return nil +} + +func (v *StateUpdateValidator) ValidateP2PBlocks(daBlocks []*types.Block, p2pBlocks []*types.Block) error { + // nothing to compare + if len(p2pBlocks) == 0 { + return nil + } + + // iterate over daBlocks and compare hashes if there block is also in p2pBlocks + i := 0 + for _, daBlock := range daBlocks { + + if p2pBlocks[i].Header.Height != daBlock.Header.Height { + break + } + p2pBlockHash, err := blockHash(p2pBlocks[i]) + if err != nil { + return err + } + daBlockHash, err := blockHash(daBlock) + if err != nil { + return err + } + if !bytes.Equal(p2pBlockHash, daBlockHash) { + return types.NewErrStateUpdateDoubleSigningFraud(daBlock.Header.Height) + } + i++ + if i == len(p2pBlocks) { + break + } + } + return nil +} + +func (v *StateUpdateValidator) ValidateDaBlocks(slBatch *settlement.ResultRetrieveBatch, daBlocks []*types.Block) error { + // check numblocks + numSlBlocks := len(slBatch.BlockDescriptors) + numDABlocks := len(daBlocks) + if numSlBlocks != numDABlocks { + return fmt.Errorf("num blocks mismatch between state update and DA batch. State index: %d State update blocks: %d DA batch blocks: %d", slBatch.StateIndex, numSlBlocks, numDABlocks) + } + + // check blocks + for i, bd := range slBatch.BlockDescriptors { + // height check + if bd.Height != daBlocks[i].Header.Height { + return types.NewErrStateUpdateHeightNotMatchingFraud(slBatch.StateIndex, bd.Height, daBlocks[i].Header.Height) + } + // we compare the state root between SL state info and DA block + if !bytes.Equal(bd.StateRoot, daBlocks[i].Header.AppHash[:]) { + return types.NewErrStateUpdateStateRootNotMatchingFraud(slBatch.StateIndex, bd.Height, bd.StateRoot, daBlocks[i].Header.AppHash[:]) + } + + // we compare the timestamp between SL state info and DA block + if !bd.Timestamp.Equal(daBlocks[i].Header.GetTimestamp()) { + return types.NewErrStateUpdateTimestampNotMatchingFraud(slBatch.StateIndex, bd.Height, bd.Timestamp, daBlocks[i].Header.GetTimestamp()) + } + } + + // TODO(srene): implement sequencer address validation + return nil +} + +// TODO(srene): implement DRS/height verification +func (v *StateUpdateValidator) validateDRS(startHeight, endHeight uint64, version string) error { + return nil +} + +// blockHash generates a hash from the block bytes to compare them +func blockHash(block *types.Block) ([]byte, error) { + blockBytes, err := block.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("error hashing block. err: %w", err) + } + h := sha256.New() + h.Write(blockBytes) + return h.Sum(nil), nil +} diff --git a/block/stateupdate_validator_test.go b/block/stateupdate_validator_test.go new file mode 100644 index 000000000..2623c2b0a --- /dev/null +++ b/block/stateupdate_validator_test.go @@ -0,0 +1,370 @@ +package block_test + +import ( + "crypto/rand" + "errors" + "testing" + "time" + + "github.com/dymensionxyz/dymint/block" + "github.com/dymensionxyz/dymint/da" + "github.com/dymensionxyz/dymint/p2p" + "github.com/dymensionxyz/dymint/settlement" + "github.com/dymensionxyz/dymint/testutil" + "github.com/dymensionxyz/dymint/types" + "github.com/dymensionxyz/dymint/version" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/libs/pubsub" + "github.com/tendermint/tendermint/proxy" +) + +func TestStateUpdateValidator_ValidateP2PBlocks(t *testing.T) { + validator := &block.StateUpdateValidator{} + + proposerKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + + batch, err := testutil.GenerateBatch(1, 10, proposerKey, "test", [32]byte{}) + require.NoError(t, err) + + doubleSignedBatch, err := testutil.GenerateBatch(1, 10, proposerKey, "test", [32]byte{}) + require.NoError(t, err) + + mixedBatch := make([]*types.Block, 10) + copy(mixedBatch, batch.Blocks) + mixedBatch[2] = doubleSignedBatch.Blocks[2] + + tests := []struct { + name string + daBlocks []*types.Block + p2pBlocks []*types.Block + expectedErrType interface{} + }{ + { + name: "Empty blocks", + daBlocks: []*types.Block{}, + p2pBlocks: []*types.Block{}, + expectedErrType: nil, + }, + { + name: "Matching blocks", + daBlocks: batch.Blocks, + p2pBlocks: batch.Blocks, + expectedErrType: nil, + }, + { + name: "double signing", + daBlocks: batch.Blocks, + p2pBlocks: doubleSignedBatch.Blocks, + expectedErrType: types.ErrStateUpdateDoubleSigningFraud{}, + }, + { + name: "mixed blocks", + daBlocks: batch.Blocks, + p2pBlocks: mixedBatch, + expectedErrType: types.ErrStateUpdateDoubleSigningFraud{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validator.ValidateP2PBlocks(tt.daBlocks, tt.p2pBlocks) + // Check the result + if tt.expectedErrType == nil { + assert.NoError(t, err) + } else { + assert.True(t, errors.As(err, &tt.expectedErrType), + "expected error of type %T, got %T", tt.expectedErrType, err) + } + }) + } +} + +func TestStateUpdateValidator_ValidateDaBlocks(t *testing.T) { + + validator := &block.StateUpdateValidator{} + + proposerKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + + batch, err := testutil.GenerateBatch(1, 2, proposerKey, "test", [32]byte{}) + require.NoError(t, err) + + tests := []struct { + name string + slBatch *settlement.ResultRetrieveBatch + daBlocks []*types.Block + expectedErrType interface{} + }{ + { + name: "Happy path - all validations pass", + slBatch: &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: []settlement.BlockDescriptor{ + {Height: 1, StateRoot: batch.Blocks[0].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + {Height: 2, StateRoot: batch.Blocks[1].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + }, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + }, + daBlocks: batch.Blocks, + expectedErrType: nil, + }, + { + name: "Error - number of blocks mismatch", + slBatch: &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: []settlement.BlockDescriptor{ + {Height: 1, StateRoot: batch.Blocks[0].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + {Height: 2, StateRoot: batch.Blocks[1].Header.AppHash[:], Timestamp: batch.Blocks[1].Header.GetTimestamp()}, + }, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + }, + daBlocks: []*types.Block{batch.Blocks[0]}, + expectedErrType: types.ErrStateUpdateHeightNotMatchingFraud{}, + }, + { + name: "Error - height mismatch", + slBatch: &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: []settlement.BlockDescriptor{ + {Height: 101, StateRoot: batch.Blocks[0].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + {Height: 102, StateRoot: batch.Blocks[1].Header.AppHash[:], Timestamp: batch.Blocks[1].Header.GetTimestamp()}, + }, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + }, + daBlocks: batch.Blocks, + expectedErrType: types.ErrStateUpdateHeightNotMatchingFraud{}, + }, + { + name: "Error - state root mismatch", + slBatch: &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: []settlement.BlockDescriptor{ + {Height: 1, StateRoot: batch.Blocks[0].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + {Height: 2, StateRoot: []byte{1, 2, 3, 4}, Timestamp: batch.Blocks[1].Header.GetTimestamp()}, + }, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + }, + daBlocks: batch.Blocks, + expectedErrType: types.ErrStateUpdateStateRootNotMatchingFraud{}, + }, + { + name: "Error - timestamp mismatch", + slBatch: &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: []settlement.BlockDescriptor{ + {Height: 1, StateRoot: batch.Blocks[0].Header.AppHash[:], Timestamp: batch.Blocks[0].Header.GetTimestamp()}, + {Height: 2, StateRoot: batch.Blocks[1].Header.AppHash[:], Timestamp: batch.Blocks[1].Header.GetTimestamp().Add(1 * time.Second)}, + }, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + }, + daBlocks: batch.Blocks, + expectedErrType: types.ErrStateUpdateTimestampNotMatchingFraud{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + // validate DA blocks + err := validator.ValidateDaBlocks(tt.slBatch, tt.daBlocks) + + // Check the result + if tt.expectedErrType == nil { + assert.NoError(t, err) + } else { + assert.True(t, errors.As(err, &tt.expectedErrType), + "expected error of type %T, got %T", tt.expectedErrType, err) + } + }) + } +} + +func TestStateUpdateValidator_ValidateStateUpdate(t *testing.T) { + + // Init app + app := testutil.GetAppMock(testutil.EndBlock) + app.On("EndBlock", mock.Anything).Return(abci.ResponseEndBlock{ + RollappParamUpdates: &abci.RollappParams{ + Da: "mock", + Version: version.Commit, + }, + ConsensusParamUpdates: &abci.ConsensusParams{ + Block: &abci.BlockParams{ + MaxGas: 40000000, + MaxBytes: 500000, + }, + }, + }) + // Create proxy app + clientCreator := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(clientCreator) + err := proxyApp.Start() + require.NoError(t, err) + chainId := "test" + proposerKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + + doubleSigned, err := testutil.GenerateBlocks(1, 10, proposerKey, chainId, [32]byte{}) + require.NoError(t, err) + + // Test cases + testCases := []struct { + name string + p2pBlocks bool + doubleSignedBlocks []*types.Block + stateUpdateFraud string + expectedErrType interface{} + }{ + { + name: "Successful validation applied from DA", + p2pBlocks: false, + doubleSignedBlocks: nil, + stateUpdateFraud: "", + expectedErrType: nil, + }, + { + name: "Successful validation applied from P2P", + p2pBlocks: true, + doubleSignedBlocks: nil, + stateUpdateFraud: "", + expectedErrType: nil, + }, + { + name: "Failed validation blocks not matching", + p2pBlocks: true, + stateUpdateFraud: "", + doubleSignedBlocks: doubleSigned, + expectedErrType: types.ErrStateUpdateDoubleSigningFraud{}, + }, + { + name: "Failed validation wrong state roots", + p2pBlocks: true, + stateUpdateFraud: "stateroot", + doubleSignedBlocks: doubleSigned, + expectedErrType: types.ErrStateUpdateStateRootNotMatchingFraud{}, + }, + { + name: "Failed validation wrong timestamps", + p2pBlocks: true, + stateUpdateFraud: "timestamp", + doubleSignedBlocks: doubleSigned, + expectedErrType: types.ErrStateUpdateTimestampNotMatchingFraud{}, + }, + { + name: "Failed validation wrong height", + p2pBlocks: true, + stateUpdateFraud: "height", + doubleSignedBlocks: doubleSigned, + expectedErrType: types.ErrStateUpdateHeightNotMatchingFraud{}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + + // Create manager + manager, err := testutil.GetManagerWithProposerKey(chainId, testutil.GetManagerConfig(), proposerKey, nil, 1, 1, 0, proxyApp, nil) + require.NoError(t, err) + require.NotNil(t, manager) + + // Create DA + manager.DAClient = testutil.GetMockDALC(log.TestingLogger()) + manager.Retriever = manager.DAClient.(da.BatchRetriever) + + // Generate batch + batch, err := testutil.GenerateBatch(1, 10, proposerKey, chainId, [32]byte{}) + assert.NoError(t, err) + + // Submit batch to DA + daResultSubmitBatch := manager.DAClient.SubmitBatch(batch) + assert.Equal(t, daResultSubmitBatch.Code, da.StatusSuccess) + + // Create block descriptors + var bds []settlement.BlockDescriptor + for _, block := range batch.Blocks { + bd := settlement.BlockDescriptor{ + Height: block.Header.Height, + StateRoot: block.Header.AppHash[:], + Timestamp: block.Header.GetTimestamp(), + } + bds = append(bds, bd) + } + + // create the batch in settlement + slBatch := &settlement.ResultRetrieveBatch{ + Batch: &settlement.Batch{ + BlockDescriptors: bds, + MetaData: &settlement.BatchMetaData{ + DA: daResultSubmitBatch.SubmitMetaData, + }, + StartHeight: 1, + EndHeight: 10, + }, + ResultBase: settlement.ResultBase{ + StateIndex: 1, + }, + } + + // Create the StateUpdateValidator + validator := block.NewStateUpdateValidator(testutil.NewLogger(t), manager) + + // set fraud data + switch tc.stateUpdateFraud { + case "stateroot": + slBatch.BlockDescriptors[0].StateRoot = []byte{} + case "timestamp": + slBatch.BlockDescriptors[0].Timestamp = slBatch.BlockDescriptors[0].Timestamp.Add(time.Second) + case "height": + slBatch.BlockDescriptors[0].Height = 2 + } + + if tc.doubleSignedBlocks != nil { + batch.Blocks = tc.doubleSignedBlocks + batch.Commits, err = testutil.GenerateCommits(batch.Blocks, proposerKey) + require.NoError(t, err) + } + + if tc.p2pBlocks { + for i, block := range batch.Blocks { + blockData := p2p.BlockData{Block: *block, Commit: *batch.Commits[i]} + msg := pubsub.NewMessage(blockData, map[string][]string{p2p.EventTypeKey: {p2p.EventNewGossipedBlock}}) + manager.OnReceivedBlock(msg) + } + } else { + manager.ProcessNextDABatch(slBatch.MetaData.DA) + } + + // validate the state update + err = validator.ValidateStateUpdate(slBatch) + + // Check the result + if tc.expectedErrType == nil { + assert.NoError(t, err) + } else { + assert.True(t, errors.As(err, &tc.expectedErrType), + "expected error of type %T, got %T", tc.expectedErrType, err) + } + + }) + } + +} diff --git a/block/submit.go b/block/submit.go index a330b2597..e6fb670f1 100644 --- a/block/submit.go +++ b/block/submit.go @@ -276,7 +276,7 @@ func (m *Manager) GetUnsubmittedBlocks() uint64 { // UpdateLastSubmittedHeight will update last height submitted height upon events. // This may be necessary in case we crashed/restarted before getting response for our submission to the settlement layer. func (m *Manager) UpdateLastSubmittedHeight(event pubsub.Message) { - eventData, ok := event.Data().(*settlement.EventDataNewBatchAccepted) + eventData, ok := event.Data().(*settlement.EventDataNewBatch) if !ok { m.logger.Error("onReceivedBatch", "err", "wrong event data received") return diff --git a/block/sync.go b/block/sync.go new file mode 100644 index 000000000..66efbe2a5 --- /dev/null +++ b/block/sync.go @@ -0,0 +1,120 @@ +package block + +import ( + "context" + "errors" + "fmt" + + "github.com/dymensionxyz/dymint/node/events" + "github.com/dymensionxyz/dymint/settlement" + uevent "github.com/dymensionxyz/dymint/utils/event" + "github.com/dymensionxyz/gerr-cosmos/gerrc" + "github.com/tendermint/tendermint/libs/pubsub" +) + +// onNewStateUpdate will update the last submitted height and will update sequencers list from SL. After, it triggers syncing or validation, depending whether it needs to sync first or only validate. +func (m *Manager) onNewStateUpdate(event pubsub.Message) { + eventData, ok := event.Data().(*settlement.EventDataNewBatch) + if !ok { + m.logger.Error("onReceivedBatch", "err", "wrong event data received") + return + } + + // Update heights based on state update end height + m.LastSubmittedHeight.Store(eventData.EndHeight) + m.UpdateTargetHeight(eventData.EndHeight) + + m.logger.Error("syncing") + + // Update sequencers list from SL + err := m.UpdateSequencerSetFromSL() + if err != nil { + m.logger.Error("update bonded sequencer set", "error", err) + } + + if eventData.EndHeight > m.State.Height() { + // Trigger syncing from DA. + m.triggerStateUpdateSyncing() + } else { + // trigger state update validation (in case no state update is applied) + m.triggerStateUpdateValidation() + } +} + +// SyncLoop listens for syncing events (from new state update or from initial syncing) and syncs to the last submitted height. +// It sends signal to validation loop for each synced state updated +func (m *Manager) SyncLoop(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case <-m.syncingC: + + m.logger.Info("syncing to target height", "targetHeight", m.LastSubmittedHeight.Load()) + + for currH := m.State.NextHeight(); currH <= m.LastSubmittedHeight.Load(); currH = m.State.NextHeight() { + // if we have the block locally, we don't need to fetch it from the DA + err := m.applyLocalBlock(currH) + if err == nil { + m.logger.Info("Synced from local", "store height", currH, "target height", m.LastSubmittedHeight.Load()) + continue + } + if !errors.Is(err, gerrc.ErrNotFound) { + m.logger.Error("Apply local block", "err", err) + } + + err = m.syncFromDABatch() + if err != nil { + m.logger.Error("process next DA batch", "err", err) + } + + // if height havent been updated, we are stuck + if m.State.NextHeight() == currH { + return fmt.Errorf("stuck at height %d", currH) + } + + m.logger.Info("Synced from DA", "store height", m.State.Height(), "target height", m.LastSubmittedHeight.Load()) + + // trigger state update validation, after each state update is applied + m.triggerStateUpdateValidation() + + err = m.attemptApplyCachedBlocks() + if err != nil { + uevent.MustPublish(context.TODO(), m.Pubsub, &events.DataHealthStatus{Error: err}, events.HealthStatusList) + m.logger.Error("Attempt apply cached blocks.", "err", err) + } + + } + + m.logger.Info("Synced.", "current height", m.State.Height(), "last submitted height", m.LastSubmittedHeight.Load()) + + m.synced.Nudge() + + } + } +} + +// waitForSyncing waits for synced nudge (in case it needs to because it was syncing) +func (m *Manager) waitForSyncing() { + if m.State.Height() < m.LastSubmittedHeight.Load() { + <-m.synced.C + } +} + +// triggerStateUpdateSyncing sends signal to channel used by syncing loop +func (m *Manager) triggerStateUpdateSyncing() { + select { + case m.syncingC <- struct{}{}: + default: + m.logger.Debug("disregarding new state update, node is still syncing") + } +} + +// triggerStateUpdateValidation sends signal to channel used by validation loop +func (m *Manager) triggerStateUpdateValidation() { + select { + case m.validateC <- struct{}{}: + default: + m.logger.Debug("disregarding new state update, node is still validating") + } +} diff --git a/block/validate.go b/block/validate.go new file mode 100644 index 000000000..fea13e533 --- /dev/null +++ b/block/validate.go @@ -0,0 +1,61 @@ +package block + +import ( + "context" + "fmt" + + "github.com/dymensionxyz/dymint/settlement" + "github.com/tendermint/tendermint/libs/pubsub" +) + +// onNewStateUpdateFinalized will update the last validated height with the last finalized height +func (m *Manager) onNewStateUpdateFinalized(event pubsub.Message) { + eventData, ok := event.Data().(*settlement.EventDataNewBatch) + if !ok { + m.logger.Error("onReceivedBatch", "err", "wrong event data received") + return + } + m.State.SetLastValidatedHeight(eventData.EndHeight) +} + +// ValidateLoop listens for syncing events (from new state update or from initial syncing) and validates state updates to the last submitted height. +func (m *Manager) ValidateLoop(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case <-m.validateC: + + m.logger.Info("validating state updates to target height", "targetHeight", m.LastSubmittedHeight.Load()) + + for currH := m.State.NextValidationHeight(); currH <= m.LastSubmittedHeight.Load(); currH = m.State.NextValidationHeight() { + + // get next batch that needs to be validated from SL + batch, err := m.SLClient.GetBatchAtHeight(currH) + if err != nil { + m.logger.Error("failed batch retrieval", "error", err) + continue + } + // validate batch + err = m.validator.ValidateStateUpdate(batch) + if err != nil { + m.FraudHandler.HandleFault(ctx, err) + } + + // this should not happen. if validation is successful m.State.NextValidationHeight() should advance. + if currH == m.State.NextValidationHeight() { + panic("validation not progressing") + } + + // update state with new validation height + _, err = m.Store.SaveState(m.State, nil) + if err != nil { + return fmt.Errorf("save state: %w", err) + } + + m.logger.Debug("state info validated", "batch end height", batch.EndHeight, "lastValidatedHeight", m.State.GetLastValidatedHeight()) + } + + } + } +} diff --git a/da/avail/avail.go b/da/avail/avail.go index a31e4a7b2..ea932c756 100644 --- a/da/avail/avail.go +++ b/da/avail/avail.go @@ -156,8 +156,9 @@ func (c *DataAvailabilityLayerClient) Stop() error { return nil } -func (c *DataAvailabilityLayerClient) Synced() <-chan struct{} { - return c.synced +// WaitForSyncing is used to check when the DA light client finished syncing +func (m *DataAvailabilityLayerClient) WaitForSyncing() { + <-m.synced } // GetClientType returns client type. diff --git a/da/celestia/celestia.go b/da/celestia/celestia.go index c9b979909..0120678ec 100644 --- a/da/celestia/celestia.go +++ b/da/celestia/celestia.go @@ -163,9 +163,9 @@ func (c *DataAvailabilityLayerClient) Stop() error { return nil } -// Started returns channel for on start event -func (c *DataAvailabilityLayerClient) Synced() <-chan struct{} { - return c.synced +// WaitForSyncing is used to check when the DA light client finished syncing +func (m *DataAvailabilityLayerClient) WaitForSyncing() { + <-m.synced } // GetClientType returns client type. diff --git a/da/da.go b/da/da.go index b07441a0d..14dec8d9f 100644 --- a/da/da.go +++ b/da/da.go @@ -216,8 +216,8 @@ type DataAvailabilityLayerClient interface { // CheckBatchAvailability checks the availability of the blob submitted getting proofs and validating them CheckBatchAvailability(daMetaData *DASubmitMetaData) ResultCheckBatch - // Channel used to check when the DA light client finished syncing - Synced() <-chan struct{} + // Used to check when the DA light client finished syncing + WaitForSyncing() // Returns the maximum allowed blob size in the DA, used to check the max batch size configured GetMaxBlobSizeBytes() uint32 diff --git a/da/grpc/grpc.go b/da/grpc/grpc.go index aff1d2819..4724d37f2 100644 --- a/da/grpc/grpc.go +++ b/da/grpc/grpc.go @@ -80,9 +80,9 @@ func (d *DataAvailabilityLayerClient) Stop() error { return d.conn.Close() } -// Synced returns channel for on sync event -func (m *DataAvailabilityLayerClient) Synced() <-chan struct{} { - return m.synced +// WaitForSyncing is used to check when the DA light client finished syncing +func (m *DataAvailabilityLayerClient) WaitForSyncing() { + <-m.synced } // GetClientType returns client type. diff --git a/da/local/local.go b/da/local/local.go index 65c63fc93..87aab4ada 100644 --- a/da/local/local.go +++ b/da/local/local.go @@ -76,9 +76,9 @@ func (m *DataAvailabilityLayerClient) Stop() error { return nil } -// Synced returns channel for on start event -func (m *DataAvailabilityLayerClient) Synced() <-chan struct{} { - return m.synced +// WaitForSyncing is used to check when the DA light client finished syncing +func (m *DataAvailabilityLayerClient) WaitForSyncing() { + <-m.synced } // GetClientType returns client type. diff --git a/mocks/github.com/dymensionxyz/dymint/da/avail/mock_SubstrateApiI.go b/mocks/github.com/dymensionxyz/dymint/da/avail/mock_SubstrateApiI.go index bba31b087..6a52c1df8 100644 --- a/mocks/github.com/dymensionxyz/dymint/da/avail/mock_SubstrateApiI.go +++ b/mocks/github.com/dymensionxyz/dymint/da/avail/mock_SubstrateApiI.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package avail diff --git a/mocks/github.com/dymensionxyz/dymint/da/celestia/types/mock_CelestiaRPCClient.go b/mocks/github.com/dymensionxyz/dymint/da/celestia/types/mock_CelestiaRPCClient.go index 5994cf817..4935cc66a 100644 --- a/mocks/github.com/dymensionxyz/dymint/da/celestia/types/mock_CelestiaRPCClient.go +++ b/mocks/github.com/dymensionxyz/dymint/da/celestia/types/mock_CelestiaRPCClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package types diff --git a/mocks/github.com/dymensionxyz/dymint/da/mock_DataAvailabilityLayerClient.go b/mocks/github.com/dymensionxyz/dymint/da/mock_DataAvailabilityLayerClient.go index 1480d557c..0e79e172c 100644 --- a/mocks/github.com/dymensionxyz/dymint/da/mock_DataAvailabilityLayerClient.go +++ b/mocks/github.com/dymensionxyz/dymint/da/mock_DataAvailabilityLayerClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package da @@ -362,49 +362,34 @@ func (_c *MockDataAvailabilityLayerClient_SubmitBatch_Call) RunAndReturn(run fun return _c } -// Synced provides a mock function with given fields: -func (_m *MockDataAvailabilityLayerClient) Synced() <-chan struct{} { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for Synced") - } - - var r0 <-chan struct{} - if rf, ok := ret.Get(0).(func() <-chan struct{}); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan struct{}) - } - } - - return r0 +// WaitForSyncing provides a mock function with given fields: +func (_m *MockDataAvailabilityLayerClient) WaitForSyncing() { + _m.Called() } -// MockDataAvailabilityLayerClient_Synced_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Synced' -type MockDataAvailabilityLayerClient_Synced_Call struct { +// MockDataAvailabilityLayerClient_WaitForSyncing_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaitForSyncing' +type MockDataAvailabilityLayerClient_WaitForSyncing_Call struct { *mock.Call } -// Synced is a helper method to define mock.On call -func (_e *MockDataAvailabilityLayerClient_Expecter) Synced() *MockDataAvailabilityLayerClient_Synced_Call { - return &MockDataAvailabilityLayerClient_Synced_Call{Call: _e.mock.On("Synced")} +// WaitForSyncing is a helper method to define mock.On call +func (_e *MockDataAvailabilityLayerClient_Expecter) WaitForSyncing() *MockDataAvailabilityLayerClient_WaitForSyncing_Call { + return &MockDataAvailabilityLayerClient_WaitForSyncing_Call{Call: _e.mock.On("WaitForSyncing")} } -func (_c *MockDataAvailabilityLayerClient_Synced_Call) Run(run func()) *MockDataAvailabilityLayerClient_Synced_Call { +func (_c *MockDataAvailabilityLayerClient_WaitForSyncing_Call) Run(run func()) *MockDataAvailabilityLayerClient_WaitForSyncing_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockDataAvailabilityLayerClient_Synced_Call) Return(_a0 <-chan struct{}) *MockDataAvailabilityLayerClient_Synced_Call { - _c.Call.Return(_a0) +func (_c *MockDataAvailabilityLayerClient_WaitForSyncing_Call) Return() *MockDataAvailabilityLayerClient_WaitForSyncing_Call { + _c.Call.Return() return _c } -func (_c *MockDataAvailabilityLayerClient_Synced_Call) RunAndReturn(run func() <-chan struct{}) *MockDataAvailabilityLayerClient_Synced_Call { +func (_c *MockDataAvailabilityLayerClient_WaitForSyncing_Call) RunAndReturn(run func()) *MockDataAvailabilityLayerClient_WaitForSyncing_Call { _c.Call.Return(run) return _c } diff --git a/mocks/github.com/dymensionxyz/dymint/settlement/dymension/mock_CosmosClient.go b/mocks/github.com/dymensionxyz/dymint/settlement/dymension/mock_CosmosClient.go index bebd10ade..4b14a3eaf 100644 --- a/mocks/github.com/dymensionxyz/dymint/settlement/dymension/mock_CosmosClient.go +++ b/mocks/github.com/dymensionxyz/dymint/settlement/dymension/mock_CosmosClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package dymension diff --git a/mocks/github.com/dymensionxyz/dymint/settlement/mock_ClientI.go b/mocks/github.com/dymensionxyz/dymint/settlement/mock_ClientI.go index e71cc9d27..7072f55c2 100644 --- a/mocks/github.com/dymensionxyz/dymint/settlement/mock_ClientI.go +++ b/mocks/github.com/dymensionxyz/dymint/settlement/mock_ClientI.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package settlement @@ -140,6 +140,64 @@ func (_c *MockClientI_GetAllSequencers_Call) RunAndReturn(run func() ([]types.Se return _c } +// GetBatchAtHeight provides a mock function with given fields: index +func (_m *MockClientI) GetBatchAtHeight(index uint64) (*settlement.ResultRetrieveBatch, error) { + ret := _m.Called(index) + + if len(ret) == 0 { + panic("no return value specified for GetBatchAtHeight") + } + + var r0 *settlement.ResultRetrieveBatch + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (*settlement.ResultRetrieveBatch, error)); ok { + return rf(index) + } + if rf, ok := ret.Get(0).(func(uint64) *settlement.ResultRetrieveBatch); ok { + r0 = rf(index) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*settlement.ResultRetrieveBatch) + } + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(index) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClientI_GetBatchAtHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBatchAtHeight' +type MockClientI_GetBatchAtHeight_Call struct { + *mock.Call +} + +// GetBatchAtHeight is a helper method to define mock.On call +// - index uint64 +func (_e *MockClientI_Expecter) GetBatchAtHeight(index interface{}) *MockClientI_GetBatchAtHeight_Call { + return &MockClientI_GetBatchAtHeight_Call{Call: _e.mock.On("GetBatchAtHeight", index)} +} + +func (_c *MockClientI_GetBatchAtHeight_Call) Run(run func(index uint64)) *MockClientI_GetBatchAtHeight_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint64)) + }) + return _c +} + +func (_c *MockClientI_GetBatchAtHeight_Call) Return(_a0 *settlement.ResultRetrieveBatch, _a1 error) *MockClientI_GetBatchAtHeight_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClientI_GetBatchAtHeight_Call) RunAndReturn(run func(uint64) (*settlement.ResultRetrieveBatch, error)) *MockClientI_GetBatchAtHeight_Call { + _c.Call.Return(run) + return _c +} + // GetBatchAtIndex provides a mock function with given fields: index func (_m *MockClientI) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, error) { ret := _m.Called(index) @@ -255,29 +313,29 @@ func (_c *MockClientI_GetBondedSequencers_Call) RunAndReturn(run func() ([]types return _c } -// GetHeightState provides a mock function with given fields: _a0 -func (_m *MockClientI) GetHeightState(_a0 uint64) (*settlement.ResultGetHeightState, error) { - ret := _m.Called(_a0) +// GetLatestBatch provides a mock function with given fields: +func (_m *MockClientI) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { + ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for GetHeightState") + panic("no return value specified for GetLatestBatch") } - var r0 *settlement.ResultGetHeightState + var r0 *settlement.ResultRetrieveBatch var r1 error - if rf, ok := ret.Get(0).(func(uint64) (*settlement.ResultGetHeightState, error)); ok { - return rf(_a0) + if rf, ok := ret.Get(0).(func() (*settlement.ResultRetrieveBatch, error)); ok { + return rf() } - if rf, ok := ret.Get(0).(func(uint64) *settlement.ResultGetHeightState); ok { - r0 = rf(_a0) + if rf, ok := ret.Get(0).(func() *settlement.ResultRetrieveBatch); ok { + r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*settlement.ResultGetHeightState) + r0 = ret.Get(0).(*settlement.ResultRetrieveBatch) } } - if rf, ok := ret.Get(1).(func(uint64) error); ok { - r1 = rf(_a0) + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() } else { r1 = ret.Error(1) } @@ -285,40 +343,39 @@ func (_m *MockClientI) GetHeightState(_a0 uint64) (*settlement.ResultGetHeightSt return r0, r1 } -// MockClientI_GetHeightState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHeightState' -type MockClientI_GetHeightState_Call struct { +// MockClientI_GetLatestBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestBatch' +type MockClientI_GetLatestBatch_Call struct { *mock.Call } -// GetHeightState is a helper method to define mock.On call -// - _a0 uint64 -func (_e *MockClientI_Expecter) GetHeightState(_a0 interface{}) *MockClientI_GetHeightState_Call { - return &MockClientI_GetHeightState_Call{Call: _e.mock.On("GetHeightState", _a0)} +// GetLatestBatch is a helper method to define mock.On call +func (_e *MockClientI_Expecter) GetLatestBatch() *MockClientI_GetLatestBatch_Call { + return &MockClientI_GetLatestBatch_Call{Call: _e.mock.On("GetLatestBatch")} } -func (_c *MockClientI_GetHeightState_Call) Run(run func(_a0 uint64)) *MockClientI_GetHeightState_Call { +func (_c *MockClientI_GetLatestBatch_Call) Run(run func()) *MockClientI_GetLatestBatch_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(uint64)) + run() }) return _c } -func (_c *MockClientI_GetHeightState_Call) Return(_a0 *settlement.ResultGetHeightState, _a1 error) *MockClientI_GetHeightState_Call { +func (_c *MockClientI_GetLatestBatch_Call) Return(_a0 *settlement.ResultRetrieveBatch, _a1 error) *MockClientI_GetLatestBatch_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClientI_GetHeightState_Call) RunAndReturn(run func(uint64) (*settlement.ResultGetHeightState, error)) *MockClientI_GetHeightState_Call { +func (_c *MockClientI_GetLatestBatch_Call) RunAndReturn(run func() (*settlement.ResultRetrieveBatch, error)) *MockClientI_GetLatestBatch_Call { _c.Call.Return(run) return _c } -// GetLatestBatch provides a mock function with given fields: -func (_m *MockClientI) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { +// GetLatestFinalizedBatch provides a mock function with given fields: +func (_m *MockClientI) GetLatestFinalizedBatch() (*settlement.ResultRetrieveBatch, error) { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for GetLatestBatch") + panic("no return value specified for GetLatestFinalizedBatch") } var r0 *settlement.ResultRetrieveBatch @@ -343,29 +400,29 @@ func (_m *MockClientI) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) return r0, r1 } -// MockClientI_GetLatestBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestBatch' -type MockClientI_GetLatestBatch_Call struct { +// MockClientI_GetLatestFinalizedBatch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestFinalizedBatch' +type MockClientI_GetLatestFinalizedBatch_Call struct { *mock.Call } -// GetLatestBatch is a helper method to define mock.On call -func (_e *MockClientI_Expecter) GetLatestBatch() *MockClientI_GetLatestBatch_Call { - return &MockClientI_GetLatestBatch_Call{Call: _e.mock.On("GetLatestBatch")} +// GetLatestFinalizedBatch is a helper method to define mock.On call +func (_e *MockClientI_Expecter) GetLatestFinalizedBatch() *MockClientI_GetLatestFinalizedBatch_Call { + return &MockClientI_GetLatestFinalizedBatch_Call{Call: _e.mock.On("GetLatestFinalizedBatch")} } -func (_c *MockClientI_GetLatestBatch_Call) Run(run func()) *MockClientI_GetLatestBatch_Call { +func (_c *MockClientI_GetLatestFinalizedBatch_Call) Run(run func()) *MockClientI_GetLatestFinalizedBatch_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockClientI_GetLatestBatch_Call) Return(_a0 *settlement.ResultRetrieveBatch, _a1 error) *MockClientI_GetLatestBatch_Call { +func (_c *MockClientI_GetLatestFinalizedBatch_Call) Return(_a0 *settlement.ResultRetrieveBatch, _a1 error) *MockClientI_GetLatestFinalizedBatch_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *MockClientI_GetLatestBatch_Call) RunAndReturn(run func() (*settlement.ResultRetrieveBatch, error)) *MockClientI_GetLatestBatch_Call { +func (_c *MockClientI_GetLatestFinalizedBatch_Call) RunAndReturn(run func() (*settlement.ResultRetrieveBatch, error)) *MockClientI_GetLatestFinalizedBatch_Call { _c.Call.Return(run) return _c } diff --git a/mocks/github.com/dymensionxyz/dymint/store/mock_Store.go b/mocks/github.com/dymensionxyz/dymint/store/mock_Store.go index 66207d239..7d4ec56d6 100644 --- a/mocks/github.com/dymensionxyz/dymint/store/mock_Store.go +++ b/mocks/github.com/dymensionxyz/dymint/store/mock_Store.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package store @@ -301,6 +301,62 @@ func (_c *MockStore_LoadBlockResponses_Call) RunAndReturn(run func(uint64) (*sta return _c } +// LoadBlockSource provides a mock function with given fields: height +func (_m *MockStore) LoadBlockSource(height uint64) (string, error) { + ret := _m.Called(height) + + if len(ret) == 0 { + panic("no return value specified for LoadBlockSource") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (string, error)); ok { + return rf(height) + } + if rf, ok := ret.Get(0).(func(uint64) string); ok { + r0 = rf(height) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStore_LoadBlockSource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LoadBlockSource' +type MockStore_LoadBlockSource_Call struct { + *mock.Call +} + +// LoadBlockSource is a helper method to define mock.On call +// - height uint64 +func (_e *MockStore_Expecter) LoadBlockSource(height interface{}) *MockStore_LoadBlockSource_Call { + return &MockStore_LoadBlockSource_Call{Call: _e.mock.On("LoadBlockSource", height)} +} + +func (_c *MockStore_LoadBlockSource_Call) Run(run func(height uint64)) *MockStore_LoadBlockSource_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint64)) + }) + return _c +} + +func (_c *MockStore_LoadBlockSource_Call) Return(_a0 string, _a1 error) *MockStore_LoadBlockSource_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStore_LoadBlockSource_Call) RunAndReturn(run func(uint64) (string, error)) *MockStore_LoadBlockSource_Call { + _c.Call.Return(run) + return _c +} + // LoadCommit provides a mock function with given fields: height func (_m *MockStore) LoadCommit(height uint64) (*types.Commit, error) { ret := _m.Called(height) @@ -817,6 +873,66 @@ func (_c *MockStore_SaveBlockResponses_Call) RunAndReturn(run func(uint64, *stat return _c } +// SaveBlockSource provides a mock function with given fields: height, source, batch +func (_m *MockStore) SaveBlockSource(height uint64, source string, batch store.KVBatch) (store.KVBatch, error) { + ret := _m.Called(height, source, batch) + + if len(ret) == 0 { + panic("no return value specified for SaveBlockSource") + } + + var r0 store.KVBatch + var r1 error + if rf, ok := ret.Get(0).(func(uint64, string, store.KVBatch) (store.KVBatch, error)); ok { + return rf(height, source, batch) + } + if rf, ok := ret.Get(0).(func(uint64, string, store.KVBatch) store.KVBatch); ok { + r0 = rf(height, source, batch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(store.KVBatch) + } + } + + if rf, ok := ret.Get(1).(func(uint64, string, store.KVBatch) error); ok { + r1 = rf(height, source, batch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStore_SaveBlockSource_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBlockSource' +type MockStore_SaveBlockSource_Call struct { + *mock.Call +} + +// SaveBlockSource is a helper method to define mock.On call +// - height uint64 +// - source string +// - batch store.KVBatch +func (_e *MockStore_Expecter) SaveBlockSource(height interface{}, source interface{}, batch interface{}) *MockStore_SaveBlockSource_Call { + return &MockStore_SaveBlockSource_Call{Call: _e.mock.On("SaveBlockSource", height, source, batch)} +} + +func (_c *MockStore_SaveBlockSource_Call) Run(run func(height uint64, source string, batch store.KVBatch)) *MockStore_SaveBlockSource_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(uint64), args[1].(string), args[2].(store.KVBatch)) + }) + return _c +} + +func (_c *MockStore_SaveBlockSource_Call) Return(_a0 store.KVBatch, _a1 error) *MockStore_SaveBlockSource_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStore_SaveBlockSource_Call) RunAndReturn(run func(uint64, string, store.KVBatch) (store.KVBatch, error)) *MockStore_SaveBlockSource_Call { + _c.Call.Return(run) + return _c +} + // SaveSequencers provides a mock function with given fields: height, seqSet, batch func (_m *MockStore) SaveSequencers(height uint64, seqSet *types.SequencerSet, batch store.KVBatch) (store.KVBatch, error) { ret := _m.Called(height, seqSet, batch) diff --git a/mocks/github.com/dymensionxyz/dymint/third_party/dymension/sequencer/types/mock_QueryClient.go b/mocks/github.com/dymensionxyz/dymint/third_party/dymension/sequencer/types/mock_QueryClient.go index 6b16285aa..fed52c6c5 100644 --- a/mocks/github.com/dymensionxyz/dymint/third_party/dymension/sequencer/types/mock_QueryClient.go +++ b/mocks/github.com/dymensionxyz/dymint/third_party/dymension/sequencer/types/mock_QueryClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package types diff --git a/mocks/github.com/dymensionxyz/dymint/types/pb/dymensionxyz/dymension/rollapp/mock_QueryClient.go b/mocks/github.com/dymensionxyz/dymint/types/pb/dymensionxyz/dymension/rollapp/mock_QueryClient.go index f182218a5..83c174f72 100644 --- a/mocks/github.com/dymensionxyz/dymint/types/pb/dymensionxyz/dymension/rollapp/mock_QueryClient.go +++ b/mocks/github.com/dymensionxyz/dymint/types/pb/dymensionxyz/dymension/rollapp/mock_QueryClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package rollapp diff --git a/mocks/github.com/tendermint/tendermint/abci/types/mock_Application.go b/mocks/github.com/tendermint/tendermint/abci/types/mock_Application.go index 45011bdc9..7393ef94e 100644 --- a/mocks/github.com/tendermint/tendermint/abci/types/mock_Application.go +++ b/mocks/github.com/tendermint/tendermint/abci/types/mock_Application.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package types diff --git a/mocks/github.com/tendermint/tendermint/proxy/mock_AppConnConsensus.go b/mocks/github.com/tendermint/tendermint/proxy/mock_AppConnConsensus.go index 9a28054e1..9ec6b2d18 100644 --- a/mocks/github.com/tendermint/tendermint/proxy/mock_AppConnConsensus.go +++ b/mocks/github.com/tendermint/tendermint/proxy/mock_AppConnConsensus.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package proxy diff --git a/mocks/github.com/tendermint/tendermint/proxy/mock_AppConns.go b/mocks/github.com/tendermint/tendermint/proxy/mock_AppConns.go index 120c2f698..affc90a4e 100644 --- a/mocks/github.com/tendermint/tendermint/proxy/mock_AppConns.go +++ b/mocks/github.com/tendermint/tendermint/proxy/mock_AppConns.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.46.0. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package proxy diff --git a/p2p/block.go b/p2p/block.go index b74f92368..d6da3da96 100644 --- a/p2p/block.go +++ b/p2p/block.go @@ -58,9 +58,7 @@ func (b *BlockData) Validate(proposerPubKey tmcrypto.PubKey) error { if err := b.Block.ValidateBasic(); err != nil { return err } - if err := b.Commit.ValidateBasic(); err != nil { - return err - } + if err := b.Commit.ValidateWithHeader(proposerPubKey, &b.Block.Header); err != nil { return err } diff --git a/proto/types/dymint/state.proto b/proto/types/dymint/state.proto index 09abbb38b..b7241aebc 100755 --- a/proto/types/dymint/state.proto +++ b/proto/types/dymint/state.proto @@ -46,6 +46,8 @@ message State { RollappParams rollapp_params = 19 [(gogoproto.nullable) = false]; bytes last_header_hash = 20; + int64 last_validated_height = 21; + } //rollapp params defined in genesis and updated via gov proposal diff --git a/settlement/dymension/dymension.go b/settlement/dymension/dymension.go index f1987c9c0..f81d62abb 100644 --- a/settlement/dymension/dymension.go +++ b/settlement/dymension/dymension.go @@ -169,7 +169,7 @@ func (c *Client) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *d return fmt.Errorf("subscription cancelled") case event := <-subscription.Out(): - eventData, _ := event.Data().(*settlement.EventDataNewBatchAccepted) + eventData, _ := event.Data().(*settlement.EventDataNewBatch) if eventData.EndHeight != batch.EndHeight() { c.logger.Debug("Received event for a different batch, ignoring.", "event", eventData) continue // continue waiting for acceptance of the current batch @@ -217,8 +217,11 @@ func (c *Client) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *d } } -func (c *Client) getStateInfo(index, height *uint64) (res *rollapptypes.QueryGetStateInfoResponse, err error) { - req := &rollapptypes.QueryGetStateInfoRequest{RollappId: c.rollappId} +func (c *Client) getStateInfo(index, height *uint64, finalized bool) (res *rollapptypes.QueryGetStateInfoResponse, err error) { + req := &rollapptypes.QueryGetStateInfoRequest{ + RollappId: c.rollappId, + Finalized: finalized, + } if index != nil { req.Index = *index } @@ -244,7 +247,16 @@ func (c *Client) getStateInfo(index, height *uint64) (res *rollapptypes.QueryGet // GetLatestBatch returns the latest batch from the Dymension Hub. func (c *Client) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { - res, err := c.getStateInfo(nil, nil) + res, err := c.getStateInfo(nil, nil, false) + if err != nil { + return nil, fmt.Errorf("get state info: %w", err) + } + return convertStateInfoToResultRetrieveBatch(&res.StateInfo) +} + +// GetLatestFinalizedBatch returns the latest finalized batch from the Dymension Hub. +func (c *Client) GetLatestFinalizedBatch() (*settlement.ResultRetrieveBatch, error) { + res, err := c.getStateInfo(nil, nil, true) if err != nil { return nil, fmt.Errorf("get state info: %w", err) } @@ -253,24 +265,20 @@ func (c *Client) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { // GetBatchAtIndex returns the batch at the given index from the Dymension Hub. func (c *Client) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, error) { - res, err := c.getStateInfo(&index, nil) + res, err := c.getStateInfo(&index, nil, false) if err != nil { return nil, fmt.Errorf("get state info: %w", err) } return convertStateInfoToResultRetrieveBatch(&res.StateInfo) } -func (c *Client) GetHeightState(h uint64) (*settlement.ResultGetHeightState, error) { - res, err := c.getStateInfo(nil, &h) +// GetBatchAtHeight returns the batch at the given height from the Dymension Hub. +func (c *Client) GetBatchAtHeight(height uint64) (*settlement.ResultRetrieveBatch, error) { + res, err := c.getStateInfo(nil, &height, false) if err != nil { return nil, fmt.Errorf("get state info: %w", err) } - return &settlement.ResultGetHeightState{ - ResultBase: settlement.ResultBase{Code: settlement.StatusSuccess}, - State: settlement.State{ - StateIndex: res.GetStateInfo().StateInfoIndex.Index, - }, - }, nil + return convertStateInfoToResultRetrieveBatch(&res.StateInfo) } // GetProposer implements settlement.ClientI. diff --git a/settlement/dymension/events.go b/settlement/dymension/events.go index 18f53cdef..be541029f 100644 --- a/settlement/dymension/events.go +++ b/settlement/dymension/events.go @@ -15,6 +15,7 @@ import ( // TODO: use types and attributes from dymension proto const ( eventStateUpdateFmt = "state_update.rollapp_id='%s' AND state_update.status='PENDING'" + eventStateUpdateFinalizedFmt = "state_update.rollapp_id='%s' AND state_update.status='FINALIZED'" eventSequencersListUpdateFmt = "create_sequencer.rollapp_id='%s'" eventRotationStartedFmt = "proposer_rotation_started.rollapp_id='%s'" ) @@ -27,6 +28,8 @@ func (c *Client) getEventData(eventType string, rawEventData ctypes.ResultEvent) return convertToNewSequencerEvent(rawEventData) case settlement.EventRotationStarted: return convertToRotationStartedEvent(rawEventData) + case settlement.EventNewBatchFinalized: + return convertToNewBatchEvent(rawEventData) } return nil, fmt.Errorf("unrecognized event type: %s", eventType) } @@ -37,12 +40,14 @@ func (c *Client) eventHandler() { eventStateUpdateQ := fmt.Sprintf(eventStateUpdateFmt, c.rollappId) eventSequencersListQ := fmt.Sprintf(eventSequencersListUpdateFmt, c.rollappId) eventRotationStartedQ := fmt.Sprintf(eventRotationStartedFmt, c.rollappId) + eventStateUpdateFinalizedQ := fmt.Sprintf(eventStateUpdateFinalizedFmt, c.rollappId) // TODO: add validation callback for the event data eventMap := map[string]string{ - eventStateUpdateQ: settlement.EventNewBatchAccepted, - eventSequencersListQ: settlement.EventNewBondedSequencer, - eventRotationStartedQ: settlement.EventRotationStarted, + eventStateUpdateQ: settlement.EventNewBatchAccepted, + eventSequencersListQ: settlement.EventNewBondedSequencer, + eventRotationStartedQ: settlement.EventRotationStarted, + eventStateUpdateFinalizedQ: settlement.EventNewBatchFinalized, } stateUpdatesC, err := c.cosmosClient.SubscribeToEvents(c.ctx, subscriber, eventStateUpdateQ, 1000) @@ -57,7 +62,10 @@ func (c *Client) eventHandler() { if err != nil { panic(fmt.Errorf("subscribe to events (%s): %w", eventRotationStartedQ, err)) } - + stateUpdatesFinalizedC, err := c.cosmosClient.SubscribeToEvents(c.ctx, subscriber, eventStateUpdateFinalizedQ, 1000) + if err != nil { + panic(fmt.Errorf("subscribe to events (%s): %w", eventStateUpdateFinalizedQ, err)) + } defer c.cosmosClient.UnsubscribeAll(c.ctx, subscriber) //nolint:errcheck for { @@ -71,6 +79,7 @@ func (c *Client) eventHandler() { case e = <-stateUpdatesC: case e = <-sequencersListC: case e = <-rotationStartedC: + case e = <-stateUpdatesFinalizedC: } c.handleReceivedEvent(e, eventMap) } @@ -94,7 +103,7 @@ func (c *Client) handleReceivedEvent(event ctypes.ResultEvent, eventMap map[stri uevent.MustPublish(c.ctx, c.pubsub, eventData, map[string][]string{settlement.EventTypeKey: {internalType}}) } -func convertToNewBatchEvent(rawEventData ctypes.ResultEvent) (*settlement.EventDataNewBatchAccepted, error) { +func convertToNewBatchEvent(rawEventData ctypes.ResultEvent) (*settlement.EventDataNewBatch, error) { var errs []error // check all expected attributes exists events := rawEventData.Events @@ -118,9 +127,10 @@ func convertToNewBatchEvent(rawEventData ctypes.ResultEvent) (*settlement.EventD return nil, errors.Join(errs...) } endHeight := uint64(startHeight + numBlocks - 1) - NewBatchEvent := &settlement.EventDataNewBatchAccepted{ - EndHeight: endHeight, - StateIndex: uint64(stateIndex), + NewBatchEvent := &settlement.EventDataNewBatch{ + StartHeight: uint64(startHeight), + EndHeight: endHeight, + StateIndex: uint64(stateIndex), } return NewBatchEvent, nil } diff --git a/settlement/dymension/utils.go b/settlement/dymension/utils.go index d071dfe9a..01ad60c6d 100644 --- a/settlement/dymension/utils.go +++ b/settlement/dymension/utils.go @@ -38,6 +38,16 @@ func convertStateInfoToResultRetrieveBatch(stateInfo *rollapptypes.StateInfo) (* if err != nil { return nil, err } + bds := []settlement.BlockDescriptor{} + for _, blockDescriptor := range stateInfo.BDs.BD { + bd := settlement.BlockDescriptor{ + Height: blockDescriptor.Height, + StateRoot: blockDescriptor.StateRoot, + Timestamp: blockDescriptor.Timestamp, + } + bds = append(bds, bd) + } + batchResult := &settlement.Batch{ Sequencer: stateInfo.Sequencer, StartHeight: stateInfo.StartHeight, @@ -45,7 +55,10 @@ func convertStateInfoToResultRetrieveBatch(stateInfo *rollapptypes.StateInfo) (* MetaData: &settlement.BatchMetaData{ DA: daMetaData, }, + DRSVersion: stateInfo.DrsVersion, + BlockDescriptors: bds, } + return &settlement.ResultRetrieveBatch{ ResultBase: settlement.ResultBase{Code: settlement.StatusSuccess, StateIndex: stateInfo.StateInfoIndex.Index}, Batch: batchResult, diff --git a/settlement/events.go b/settlement/events.go index f5e31f8ef..2ff811410 100644 --- a/settlement/events.go +++ b/settlement/events.go @@ -14,6 +14,7 @@ const ( EventNewBatchAccepted = "NewBatchAccepted" EventNewBondedSequencer = "NewBondedSequencer" EventRotationStarted = "RotationStarted" + EventNewBatchFinalized = "NewBatchFinalized" ) // Convenience objects @@ -21,25 +22,28 @@ var ( EventNewBatchAcceptedList = map[string][]string{EventTypeKey: {EventNewBatchAccepted}} EventNewBondedSequencerList = map[string][]string{EventTypeKey: {EventNewBondedSequencer}} EventRotationStartedList = map[string][]string{EventTypeKey: {EventRotationStarted}} + EventNewBatchFinalizedList = map[string][]string{EventTypeKey: {EventNewBatchFinalized}} ) // Queries var ( - EventQueryNewSettlementBatchAccepted = uevent.QueryFor(EventTypeKey, EventNewBatchAccepted) - EventQueryNewBondedSequencer = uevent.QueryFor(EventTypeKey, EventNewBondedSequencer) - EventQueryRotationStarted = uevent.QueryFor(EventTypeKey, EventRotationStarted) + EventQueryNewSettlementBatchAccepted = uevent.QueryFor(EventTypeKey, EventNewBatchAccepted) + EventQueryNewSettlementBatchFinalized = uevent.QueryFor(EventTypeKey, EventNewBatchFinalized) + EventQueryNewBondedSequencer = uevent.QueryFor(EventTypeKey, EventNewBondedSequencer) + EventQueryRotationStarted = uevent.QueryFor(EventTypeKey, EventRotationStarted) ) // Data -type EventDataNewBatchAccepted struct { +type EventDataNewBatch struct { + StartHeight uint64 // EndHeight is the height of the last accepted batch EndHeight uint64 // StateIndex is the rollapp-specific index the batch was saved in the SL StateIndex uint64 } -func (e EventDataNewBatchAccepted) String() string { +func (e EventDataNewBatch) String() string { return fmt.Sprintf("EndHeight: %d, StateIndex: %d", e.EndHeight, e.StateIndex) } diff --git a/settlement/grpc/grpc.go b/settlement/grpc/grpc.go index 67c62e0d3..723aae1d0 100644 --- a/settlement/grpc/grpc.go +++ b/settlement/grpc/grpc.go @@ -152,7 +152,7 @@ func (c *Client) Start() error { if err != nil { panic(err) } - err = c.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewBatchAccepted{EndHeight: b.EndHeight}, settlement.EventNewBatchAcceptedList) + err = c.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewBatch{EndHeight: b.EndHeight}, settlement.EventNewBatchAcceptedList) if err != nil { panic(err) } @@ -181,7 +181,7 @@ func (c *Client) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *d } time.Sleep(10 * time.Millisecond) // mimic a delay in batch acceptance - err = c.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewBatchAccepted{EndHeight: settlementBatch.EndHeight}, settlement.EventNewBatchAcceptedList) + err = c.pubsub.PublishWithEvents(context.Background(), &settlement.EventDataNewBatch{EndHeight: settlementBatch.EndHeight}, settlement.EventNewBatchAcceptedList) if err != nil { return err } @@ -198,6 +198,11 @@ func (c *Client) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { return batchResult, nil } +// GetLatestFinalizedBatch returns the latest finalized batch from the kv store +func (c *Client) GetLatestFinalizedBatch() (*settlement.ResultRetrieveBatch, error) { + return nil, gerrc.ErrNotFound +} + // GetBatchAtIndex returns the batch at the given index func (c *Client) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, error) { batchResult, err := c.retrieveBatchAtStateIndex(index) @@ -209,7 +214,7 @@ func (c *Client) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, return batchResult, nil } -func (c *Client) GetHeightState(index uint64) (*settlement.ResultGetHeightState, error) { +func (c *Client) GetBatchAtHeight(height uint64) (*settlement.ResultRetrieveBatch, error) { panic("hub grpc client get height state is not implemented: implement me") // TODO: impl } @@ -273,6 +278,16 @@ func (c *Client) saveBatch(batch *settlement.Batch) error { } func (c *Client) convertBatchtoSettlementBatch(batch *types.Batch, daResult *da.ResultSubmitBatch) *settlement.Batch { + bds := []settlement.BlockDescriptor{} + for _, block := range batch.Blocks { + bd := settlement.BlockDescriptor{ + Height: block.Header.Height, + StateRoot: block.Header.AppHash[:], + Timestamp: block.Header.GetTimestamp(), + } + bds = append(bds, bd) + } + settlementBatch := &settlement.Batch{ Sequencer: c.GetProposer().SettlementAddress, StartHeight: batch.StartHeight(), @@ -283,10 +298,9 @@ func (c *Client) convertBatchtoSettlementBatch(batch *types.Batch, daResult *da. Client: daResult.SubmitMetaData.Client, }, }, + BlockDescriptors: bds, } - for _, block := range batch.Blocks { - settlementBatch.AppHashes = append(settlementBatch.AppHashes, block.Header.AppHash) - } + return settlementBatch } diff --git a/settlement/local/local.go b/settlement/local/local.go index b085a567b..c548d0bd1 100644 --- a/settlement/local/local.go +++ b/settlement/local/local.go @@ -142,7 +142,7 @@ func (c *Client) SubmitBatch(batch *types.Batch, daClient da.Client, daResult *d time.Sleep(100 * time.Millisecond) // mimic a delay in batch acceptance ctx := context.Background() - uevent.MustPublish(ctx, c.pubsub, settlement.EventDataNewBatchAccepted{EndHeight: settlementBatch.EndHeight}, settlement.EventNewBatchAcceptedList) + uevent.MustPublish(ctx, c.pubsub, settlement.EventDataNewBatch{EndHeight: settlementBatch.EndHeight}, settlement.EventNewBatchAcceptedList) return nil } @@ -159,6 +159,11 @@ func (c *Client) GetLatestBatch() (*settlement.ResultRetrieveBatch, error) { return batchResult, nil } +// GetLatestFinalizedBatch returns the latest finalized batch from the kv store +func (c *Client) GetLatestFinalizedBatch() (*settlement.ResultRetrieveBatch, error) { + return nil, gerrc.ErrNotFound // TODO: need to return a cosmos specific error? +} + // GetBatchAtIndex returns the batch at the given index func (c *Client) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, error) { batchResult, err := c.retrieveBatchAtStateIndex(index) @@ -170,22 +175,19 @@ func (c *Client) GetBatchAtIndex(index uint64) (*settlement.ResultRetrieveBatch, return batchResult, nil } -func (c *Client) GetHeightState(h uint64) (*settlement.ResultGetHeightState, error) { +func (c *Client) GetBatchAtHeight(h uint64) (*settlement.ResultRetrieveBatch, error) { c.mu.Lock() defer c.mu.Unlock() // TODO: optimize (binary search, or just make another index) for i := c.slStateIndex; i > 0; i-- { b, err := c.GetBatchAtIndex(i) if err != nil { - return nil, err + return &settlement.ResultRetrieveBatch{ + ResultBase: settlement.ResultBase{Code: settlement.StatusError, Message: err.Error()}, + }, err } if b.StartHeight <= h && b.EndHeight >= h { - return &settlement.ResultGetHeightState{ - ResultBase: settlement.ResultBase{Code: settlement.StatusSuccess}, - State: settlement.State{ - StateIndex: i, - }, - }, nil + return b, nil } } return nil, gerrc.ErrNotFound // TODO: need to return a cosmos specific error? @@ -267,6 +269,16 @@ func (c *Client) retrieveBatchAtStateIndex(slStateIndex uint64) (*settlement.Res } func (c *Client) convertBatchToSettlementBatch(batch *types.Batch, daResult *da.ResultSubmitBatch) *settlement.Batch { + bds := []settlement.BlockDescriptor{} + for _, block := range batch.Blocks { + bd := settlement.BlockDescriptor{ + Height: block.Header.Height, + StateRoot: block.Header.AppHash[:], + Timestamp: block.Header.GetTimestamp(), + } + bds = append(bds, bd) + } + settlementBatch := &settlement.Batch{ Sequencer: c.GetProposer().SettlementAddress, StartHeight: batch.StartHeight(), @@ -277,10 +289,9 @@ func (c *Client) convertBatchToSettlementBatch(batch *types.Batch, daResult *da. Client: daResult.SubmitMetaData.Client, }, }, + BlockDescriptors: bds, } - for _, block := range batch.Blocks { - settlementBatch.AppHashes = append(settlementBatch.AppHashes, block.Header.AppHash) - } + return settlementBatch } diff --git a/settlement/local/local_test.go b/settlement/local/local_test.go index ce6bcd46a..ef71e123b 100644 --- a/settlement/local/local_test.go +++ b/settlement/local/local_test.go @@ -76,11 +76,11 @@ func TestSubmitBatch(t *testing.T) { require.NoError(err) assert.Equal(batch1.EndHeight(), queriedBatch.Batch.EndHeight) - state, err := sllayer.GetHeightState(1) + queriedBatchAtState, err := sllayer.GetBatchAtHeight(1) require.NoError(err) - assert.Equal(queriedBatch.StateIndex, state.State.StateIndex) + assert.Equal(queriedBatch.StateIndex, queriedBatchAtState.StateIndex) - queriedBatch, err = sllayer.GetBatchAtIndex(state.State.StateIndex) + queriedBatch, err = sllayer.GetBatchAtIndex(queriedBatchAtState.StateIndex) require.NoError(err) assert.Equal(batch1.EndHeight(), queriedBatch.Batch.EndHeight) @@ -94,11 +94,11 @@ func TestSubmitBatch(t *testing.T) { require.NoError(err) assert.Equal(batch2.EndHeight(), queriedBatch.Batch.EndHeight) - state, err = sllayer.GetHeightState(2) + queriedBatchAtState, err = sllayer.GetBatchAtHeight(2) require.NoError(err) - assert.Equal(queriedBatch.StateIndex, state.State.StateIndex) + assert.Equal(queriedBatch.StateIndex, queriedBatchAtState.StateIndex) - queriedBatch, err = sllayer.GetBatchAtIndex(state.State.StateIndex) + queriedBatch, err = sllayer.GetBatchAtIndex(queriedBatchAtState.StateIndex) require.NoError(err) assert.Equal(batch2.EndHeight(), queriedBatch.Batch.EndHeight) diff --git a/settlement/settlement.go b/settlement/settlement.go index 91dea7fdb..716601123 100644 --- a/settlement/settlement.go +++ b/settlement/settlement.go @@ -1,6 +1,8 @@ package settlement import ( + "time" + "github.com/dymensionxyz/dymint/da" "github.com/dymensionxyz/dymint/types" "github.com/tendermint/tendermint/libs/pubsub" @@ -31,14 +33,24 @@ type BatchMetaData struct { DA *da.DASubmitMetaData } +type BlockDescriptor struct { + // height is the height of the block + Height uint64 + // stateRoot is a 32 byte array of the hash of the block (state root of the block) + StateRoot []byte + // timestamp is the time from the block header + Timestamp time.Time +} + type Batch struct { // sequencer is the bech32-encoded address of the sequencer sent the update - Sequencer string - StartHeight uint64 - EndHeight uint64 - AppHashes [][32]byte + Sequencer string + StartHeight uint64 + EndHeight uint64 + BlockDescriptors []BlockDescriptor // MetaData about the batch in the DA layer - MetaData *BatchMetaData + MetaData *BatchMetaData + DRSVersion string } type ResultRetrieveBatch struct { @@ -73,7 +85,10 @@ type ClientI interface { GetLatestBatch() (*ResultRetrieveBatch, error) // GetBatchAtIndex returns the batch at the given index. GetBatchAtIndex(index uint64) (*ResultRetrieveBatch, error) - + // GetBatchAtHeight returns the batch at the given height. + GetBatchAtHeight(index uint64) (*ResultRetrieveBatch, error) + // GetLatestFinalizedBatch returns the latest finalized batch from the settlement layer. + GetLatestFinalizedBatch() (*ResultRetrieveBatch, error) // GetAllSequencers returns all sequencers for this rollapp (bonded and not bonded). GetAllSequencers() ([]types.Sequencer, error) // GetBondedSequencers returns the list of the bonded sequencers for this rollapp. @@ -84,6 +99,4 @@ type ClientI interface { // CheckRotationInProgress returns the next proposer for this chain in case of a rotation. // If no rotation is in progress, it should return nil. CheckRotationInProgress() (*types.Sequencer, error) - - GetHeightState(uint64) (*ResultGetHeightState, error) } diff --git a/store/store.go b/store/store.go index 9bdb8075d..a236faeb5 100644 --- a/store/store.go +++ b/store/store.go @@ -23,6 +23,7 @@ var ( responsesPrefix = [1]byte{5} sequencersPrefix = [1]byte{6} cidPrefix = [1]byte{7} + sourcePrefix = [1]byte{8} ) // DefaultStore is a default store implementation. @@ -116,6 +117,25 @@ func (s *DefaultStore) LoadBlockByHash(hash [32]byte) (*types.Block, error) { return block, nil } +// SaveBlockValidation saves block validation in Store. +func (s *DefaultStore) SaveBlockSource(height uint64, source string, batch KVBatch) (KVBatch, error) { + if batch == nil { + return nil, s.db.Set(getSourceKey(height), []byte(source)) + } + err := batch.Set(getSourceKey(height), []byte(source)) + return batch, err +} + +// LoadBlockValidation returns block validation in Store. +func (s *DefaultStore) LoadBlockSource(height uint64) (string, error) { + source, err := s.db.Get(getSourceKey(height)) + if err != nil { + return "", fmt.Errorf("retrieve block results from height %v: %w", height, err) + } + + return string(source[:]), nil +} + // SaveBlockResponses saves block responses (events, tx responses, etc) in Store. func (s *DefaultStore) SaveBlockResponses(height uint64, responses *tmstate.ABCIResponses, batch KVBatch) (KVBatch, error) { data, err := responses.Marshal() @@ -332,3 +352,9 @@ func getCidKey(height uint64) []byte { binary.BigEndian.PutUint64(buf, height) return append(cidPrefix[:], buf[:]...) } + +func getSourceKey(height uint64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, height) + return append(sourcePrefix[:], buf[:]...) +} diff --git a/store/storeIface.go b/store/storeIface.go index 3061fc936..5db492588 100644 --- a/store/storeIface.go +++ b/store/storeIface.go @@ -78,4 +78,8 @@ type Store interface { SaveBlockCid(height uint64, cid cid.Cid, batch KVBatch) (KVBatch, error) LoadBlockCid(height uint64) (cid.Cid, error) + + SaveBlockSource(height uint64, source string, batch KVBatch) (KVBatch, error) + + LoadBlockSource(height uint64) (string, error) } diff --git a/testutil/types.go b/testutil/types.go index 812cd935a..365538e9f 100644 --- a/testutil/types.go +++ b/testutil/types.go @@ -27,7 +27,7 @@ const ( func createRandomHashes() [][32]byte { h := [][32]byte{} - for i := 0; i < 8; i++ { + for i := 0; i < 4; i++ { var h1 [32]byte _, err := rand.Read(h1[:]) if err != nil { @@ -63,9 +63,9 @@ func generateBlock(height uint64, lastHeaderHash [32]byte, proposerHash []byte, Height: height, Time: 4567, LastHeaderHash: lastHeaderHash, - LastCommitHash: h[1], - DataHash: h[2], - ConsensusHash: h[3], + LastCommitHash: h[0], + DataHash: h[1], + ConsensusHash: h[2], AppHash: [32]byte{}, LastResultsHash: GetEmptyLastResultsHash(), ProposerAddress: proposerAddress, @@ -80,7 +80,7 @@ func generateBlock(height uint64, lastHeaderHash [32]byte, proposerHash []byte, }, LastCommit: types.Commit{ Height: 8, - HeaderHash: h[7], + HeaderHash: h[3], Signatures: []types.Signature{}, }, } diff --git a/types/errors.go b/types/errors.go index f304b9d3b..96e8fd897 100644 --- a/types/errors.go +++ b/types/errors.go @@ -1,6 +1,7 @@ package types import ( + "encoding/hex" "errors" "fmt" "time" @@ -312,3 +313,91 @@ func (e ErrInvalidHeaderDataHashFraud) Error() string { func (e ErrInvalidHeaderDataHashFraud) Unwrap() error { return gerrc.ErrFault } + +type ErrStateUpdateHeightNotMatchingFraud struct { + StateIndex uint64 + SLHeight uint64 + DAHeight uint64 +} + +func NewErrStateUpdateHeightNotMatchingFraud(stateIndex uint64, slHeight uint64, daHeight uint64) error { + return &ErrStateUpdateHeightNotMatchingFraud{ + StateIndex: stateIndex, + SLHeight: slHeight, + DAHeight: daHeight, + } +} + +func (e ErrStateUpdateHeightNotMatchingFraud) Error() string { + return fmt.Sprintf("block height in DA batch not matching SL batch height. StateIndex: %d SLHeight: %d DAHeight: %d", e.StateIndex, e.SLHeight, e.DAHeight) +} + +func (e ErrStateUpdateHeightNotMatchingFraud) Unwrap() error { + return gerrc.ErrFault +} + +type ErrStateUpdateStateRootNotMatchingFraud struct { + StateIndex uint64 + Height uint64 + SLStateRoot []byte + DAStateRoot []byte +} + +func NewErrStateUpdateStateRootNotMatchingFraud(stateIndex uint64, height uint64, slStateRoot []byte, daStateRoot []byte) error { + return &ErrStateUpdateStateRootNotMatchingFraud{ + StateIndex: stateIndex, + Height: height, + SLStateRoot: slStateRoot, + DAStateRoot: daStateRoot, + } +} + +func (e ErrStateUpdateStateRootNotMatchingFraud) Error() string { + return fmt.Sprintf("state root in DA batch block not matching state root in SL. StateIndex: %d Height: %d SLTimestsamp: %s DA:Timestamp: %s", e.StateIndex, e.Height, hex.EncodeToString(e.SLStateRoot), hex.EncodeToString(e.DAStateRoot)) +} + +func (e ErrStateUpdateStateRootNotMatchingFraud) Unwrap() error { + return gerrc.ErrFault +} + +type ErrStateUpdateTimestampNotMatchingFraud struct { + StateIndex uint64 + Height uint64 + SLTimestamp time.Time + DATimestamp time.Time +} + +func NewErrStateUpdateTimestampNotMatchingFraud(stateIndex uint64, height uint64, slTimestamp time.Time, daTimestamp time.Time) error { + return &ErrStateUpdateTimestampNotMatchingFraud{ + StateIndex: stateIndex, + Height: height, + SLTimestamp: slTimestamp, + DATimestamp: daTimestamp, + } +} + +func (e ErrStateUpdateTimestampNotMatchingFraud) Error() string { + return fmt.Sprintf("timestamp in DA batch block not matching timestamp in SL. StateIndex: %d Height: %d SLTimestsamp: %s DA:Timestamp: %s", e.StateIndex, e.Height, e.SLTimestamp, e.DATimestamp) +} + +func (e ErrStateUpdateTimestampNotMatchingFraud) Unwrap() error { + return gerrc.ErrFault +} + +type ErrStateUpdateDoubleSigningFraud struct { + Height uint64 +} + +func NewErrStateUpdateDoubleSigningFraud(height uint64) error { + return &ErrStateUpdateDoubleSigningFraud{ + Height: height, + } +} + +func (e ErrStateUpdateDoubleSigningFraud) Error() string { + return fmt.Sprintf("block received from P2P not matching block found in DA. Height: %d", e.Height) +} + +func (e ErrStateUpdateDoubleSigningFraud) Unwrap() error { + return gerrc.ErrFault +} diff --git a/types/pb/dymint/state.pb.go b/types/pb/dymint/state.pb.go index 33c2a1de3..823a62c80 100644 --- a/types/pb/dymint/state.pb.go +++ b/types/pb/dymint/state.pb.go @@ -48,6 +48,7 @@ type State struct { SequencerSet SequencerSet `protobuf:"bytes,18,opt,name=sequencerSet,proto3" json:"sequencerSet"` RollappParams RollappParams `protobuf:"bytes,19,opt,name=rollapp_params,json=rollappParams,proto3" json:"rollapp_params"` LastHeaderHash []byte `protobuf:"bytes,20,opt,name=last_header_hash,json=lastHeaderHash,proto3" json:"last_header_hash,omitempty"` + LastValidatedHeight int64 `protobuf:"varint,21,opt,name=last_validated_height,json=lastValidatedHeight,proto3" json:"last_validated_height,omitempty"` } func (m *State) Reset() { *m = State{} } @@ -203,6 +204,13 @@ func (m *State) GetLastHeaderHash() []byte { return nil } +func (m *State) GetLastValidatedHeight() int64 { + if m != nil { + return m.LastValidatedHeight + } + return 0 +} + //rollapp params defined in genesis and updated via gov proposal type RollappParams struct { //data availability type (e.g. celestia) used in the rollapp @@ -266,51 +274,53 @@ func init() { func init() { proto.RegisterFile("types/dymint/state.proto", fileDescriptor_4b679420add07272) } var fileDescriptor_4b679420add07272 = []byte{ - // 701 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0xcd, 0x4e, 0xdb, 0x4a, - 0x18, 0x8d, 0x43, 0x20, 0xce, 0x84, 0xfc, 0x30, 0x70, 0x25, 0xc3, 0x95, 0x9c, 0xc0, 0xbd, 0xad, - 0xa2, 0x2e, 0x1c, 0xa9, 0xac, 0xba, 0x69, 0x25, 0xc3, 0x82, 0x20, 0x54, 0x55, 0x4e, 0xc5, 0xa2, - 0x1b, 0x6b, 0x6c, 0x4f, 0xed, 0x51, 0x1d, 0x8f, 0xeb, 0x99, 0xa0, 0xd2, 0xa7, 0xe0, 0xb1, 0x58, - 0xb2, 0xec, 0x8a, 0x56, 0xe1, 0x45, 0xaa, 0xf9, 0x71, 0xe2, 0x10, 0xb1, 0x4a, 0xe6, 0x7c, 0x67, - 0x8e, 0xcf, 0x9c, 0xef, 0x9b, 0x01, 0x16, 0xbf, 0xcd, 0x31, 0x1b, 0x47, 0xb7, 0x33, 0x92, 0xf1, - 0x31, 0xe3, 0x88, 0x63, 0x27, 0x2f, 0x28, 0xa7, 0x70, 0x47, 0x61, 0x47, 0x07, 0x31, 0x8d, 0xa9, - 0x84, 0xc6, 0xe2, 0x9f, 0xaa, 0x1e, 0x0d, 0x62, 0x4a, 0xe3, 0x14, 0x8f, 0xe5, 0x2a, 0x98, 0x7f, - 0x1d, 0x73, 0x32, 0xc3, 0x8c, 0xa3, 0x59, 0xae, 0x09, 0xc7, 0x4a, 0x98, 0xe3, 0x2c, 0xc2, 0x85, - 0x14, 0x47, 0x41, 0x48, 0xc6, 0x12, 0xd5, 0x94, 0x93, 0x0d, 0x8a, 0x06, 0x2a, 0x9c, 0xd7, 0x2f, - 0x70, 0x6e, 0x50, 0x4a, 0x22, 0xc4, 0x69, 0xa1, 0x79, 0xff, 0xbd, 0xc0, 0xcb, 0x51, 0x81, 0x66, - 0x2f, 0x7f, 0x50, 0x1e, 0x78, 0xed, 0x83, 0x87, 0x6b, 0x81, 0xa8, 0x1f, 0x55, 0x3a, 0x59, 0x34, - 0xc1, 0xf6, 0x54, 0x6c, 0x80, 0xa7, 0xa0, 0x79, 0x83, 0x0b, 0x46, 0x68, 0x66, 0x19, 0x43, 0x63, - 0xd4, 0x7e, 0x7b, 0xe8, 0xac, 0x44, 0x1d, 0x95, 0xe2, 0xb5, 0x22, 0x78, 0x25, 0x13, 0x1e, 0x02, - 0x33, 0x4c, 0x10, 0xc9, 0x7c, 0x12, 0x59, 0xf5, 0xa1, 0x31, 0x6a, 0x79, 0x4d, 0xb9, 0x9e, 0x44, - 0xf0, 0x15, 0xe8, 0x92, 0x8c, 0x70, 0x82, 0x52, 0x3f, 0xc1, 0x24, 0x4e, 0xb8, 0xb5, 0x35, 0x34, - 0x46, 0x5b, 0x5e, 0x47, 0xa3, 0x17, 0x12, 0x84, 0x6f, 0xc0, 0x5e, 0x8a, 0x18, 0xf7, 0x83, 0x94, - 0x86, 0xdf, 0x4a, 0x66, 0x43, 0x32, 0x7b, 0xa2, 0xe0, 0x0a, 0x5c, 0x73, 0x3d, 0xd0, 0xa9, 0x70, - 0x49, 0x64, 0x6d, 0x6f, 0x1a, 0x55, 0xe7, 0x96, 0xbb, 0x26, 0xe7, 0xee, 0xfe, 0xfd, 0xe3, 0xa0, - 0xb6, 0x78, 0x1c, 0xb4, 0xaf, 0x4a, 0xa9, 0xc9, 0xb9, 0xd7, 0x5e, 0xea, 0x4e, 0x22, 0x78, 0x05, - 0x7a, 0x15, 0x4d, 0xd1, 0x71, 0x6b, 0x47, 0xaa, 0x1e, 0x39, 0x6a, 0x1c, 0x9c, 0x72, 0x1c, 0x9c, - 0xcf, 0xe5, 0x38, 0xb8, 0xa6, 0x90, 0xbd, 0xfb, 0x3d, 0x30, 0xbc, 0xce, 0x52, 0x4b, 0x54, 0xa1, - 0x0b, 0xc0, 0xb2, 0x8b, 0xcc, 0x6a, 0x49, 0x21, 0x7b, 0xd3, 0xde, 0x75, 0xc9, 0x99, 0x62, 0xee, - 0xd6, 0x2d, 0xc3, 0xab, 0xec, 0x82, 0x67, 0xc0, 0x96, 0x8e, 0x54, 0x16, 0xfe, 0xaa, 0xe2, 0x87, - 0x09, 0xca, 0x62, 0x1c, 0x59, 0x6d, 0x19, 0xcf, 0xbf, 0x82, 0xa5, 0x92, 0x59, 0xea, 0xb1, 0x33, - 0x45, 0x81, 0x1e, 0xe8, 0x87, 0x34, 0x63, 0x38, 0x63, 0x73, 0xe6, 0xab, 0x81, 0xb1, 0x76, 0xa5, - 0x9d, 0xe3, 0x4d, 0x3b, 0x67, 0x25, 0xf3, 0x93, 0x24, 0xba, 0x0d, 0x71, 0x3c, 0xaf, 0x17, 0xae, - 0xc3, 0xf0, 0x23, 0xf8, 0xbf, 0x6a, 0xec, 0xb9, 0xfe, 0xd2, 0x5e, 0x47, 0xda, 0x1b, 0xae, 0xec, - 0x3d, 0xd3, 0x2f, 0x3d, 0x96, 0xad, 0x2f, 0x30, 0x9b, 0xa7, 0x9c, 0xf9, 0x09, 0x62, 0x89, 0xd5, - 0x1d, 0x1a, 0xa3, 0x5d, 0xd5, 0x7a, 0x4f, 0xe1, 0x17, 0x88, 0x25, 0x62, 0xd0, 0x50, 0x9e, 0x2b, - 0x4a, 0x4f, 0x52, 0x9a, 0x28, 0xcf, 0x65, 0xe9, 0x83, 0x96, 0x61, 0x9c, 0x16, 0xb8, 0x9c, 0xa0, - 0xfe, 0xd0, 0x18, 0x35, 0xdc, 0xfd, 0xc5, 0xe3, 0xa0, 0x27, 0x5a, 0x3f, 0x15, 0x35, 0x65, 0x46, - 0x69, 0x57, 0x00, 0x38, 0x00, 0xed, 0x00, 0xb1, 0xe5, 0xd6, 0x3d, 0xb1, 0xd5, 0x03, 0x02, 0xd2, - 0x84, 0xf7, 0x60, 0x97, 0xe1, 0xef, 0x73, 0x9c, 0x85, 0x58, 0x74, 0xcc, 0x82, 0x32, 0xc8, 0x03, - 0x47, 0xdf, 0xa4, 0x69, 0xa5, 0xa6, 0xb3, 0x5b, 0xe3, 0x43, 0x17, 0x74, 0x0b, 0x9a, 0xa6, 0xe2, - 0x00, 0xba, 0x15, 0xfb, 0x52, 0xe1, 0x9f, 0x52, 0xc1, 0x53, 0xd5, 0xb5, 0xf8, 0x3b, 0x45, 0x15, - 0x84, 0x23, 0xd0, 0xd7, 0xe1, 0xa3, 0x08, 0x17, 0x2a, 0x88, 0x03, 0x19, 0x44, 0x57, 0x05, 0x2d, - 0x60, 0x91, 0xc7, 0x65, 0xc3, 0x6c, 0xf6, 0xcd, 0xcb, 0x86, 0x69, 0xf6, 0x5b, 0x97, 0x0d, 0x13, - 0xf4, 0xdb, 0x27, 0xef, 0x40, 0x67, 0xed, 0x0b, 0xb0, 0x0b, 0xea, 0x11, 0x92, 0xd7, 0xbc, 0xe5, - 0xd5, 0x23, 0x04, 0xad, 0xd5, 0xdd, 0xd7, 0xb7, 0x58, 0x2f, 0xdd, 0x8b, 0xfb, 0x85, 0x6d, 0x3c, - 0x2c, 0x6c, 0xe3, 0xcf, 0xc2, 0x36, 0xee, 0x9e, 0xec, 0xda, 0xc3, 0x93, 0x5d, 0xfb, 0xf5, 0x64, - 0xd7, 0xbe, 0x38, 0x31, 0xe1, 0xc9, 0x3c, 0x70, 0x42, 0x3a, 0x13, 0x4f, 0x0a, 0xce, 0x04, 0xff, - 0xc7, 0xed, 0xcf, 0xf2, 0x99, 0xd1, 0x6f, 0x55, 0xa0, 0xd7, 0xc1, 0x8e, 0xbc, 0x47, 0xa7, 0x7f, - 0x03, 0x00, 0x00, 0xff, 0xff, 0x76, 0xee, 0xb0, 0xc3, 0x9e, 0x05, 0x00, 0x00, + // 721 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x54, 0xcd, 0x4e, 0xdb, 0x4c, + 0x14, 0x8d, 0x43, 0x20, 0xce, 0x84, 0xfc, 0x30, 0x01, 0xc9, 0xf0, 0x49, 0x4e, 0xe0, 0x6b, 0xab, + 0xa8, 0x0b, 0x47, 0x82, 0x55, 0x37, 0xad, 0x64, 0x58, 0x10, 0x84, 0xaa, 0xca, 0xa9, 0x58, 0x74, + 0x63, 0x8d, 0xed, 0xa9, 0x6d, 0xd5, 0xb1, 0x5d, 0xcf, 0x04, 0x95, 0x3e, 0x05, 0xab, 0x3e, 0x13, + 0x4b, 0x96, 0x5d, 0xd1, 0x2a, 0xbc, 0x48, 0x35, 0x7f, 0x89, 0x43, 0x94, 0x55, 0x32, 0xe7, 0x9e, + 0x39, 0x3e, 0x73, 0xee, 0x9d, 0x01, 0x06, 0xbd, 0xcb, 0x31, 0x19, 0x05, 0x77, 0xd3, 0x38, 0xa5, + 0x23, 0x42, 0x11, 0xc5, 0x56, 0x5e, 0x64, 0x34, 0x83, 0x3b, 0x02, 0x3b, 0xda, 0x0f, 0xb3, 0x30, + 0xe3, 0xd0, 0x88, 0xfd, 0x13, 0xd5, 0xa3, 0x7e, 0x98, 0x65, 0x61, 0x82, 0x47, 0x7c, 0xe5, 0xcd, + 0xbe, 0x8e, 0x68, 0x3c, 0xc5, 0x84, 0xa2, 0x69, 0x2e, 0x09, 0xc7, 0x42, 0x98, 0xe2, 0x34, 0xc0, + 0x05, 0x17, 0x47, 0x9e, 0x1f, 0x8f, 0x38, 0x2a, 0x29, 0x27, 0x6b, 0x14, 0x09, 0x94, 0x38, 0x6f, + 0x36, 0x70, 0x6e, 0x51, 0x12, 0x07, 0x88, 0x66, 0x85, 0xe4, 0xfd, 0xbf, 0x81, 0x97, 0xa3, 0x02, + 0x4d, 0x37, 0x7f, 0x90, 0x1f, 0x78, 0xe5, 0x83, 0x87, 0x2b, 0x81, 0x88, 0x1f, 0x51, 0x3a, 0xf9, + 0xa5, 0x83, 0xed, 0x09, 0xdb, 0x00, 0xcf, 0x40, 0xfd, 0x16, 0x17, 0x24, 0xce, 0x52, 0x43, 0x1b, + 0x68, 0xc3, 0xe6, 0xe9, 0xa1, 0xb5, 0x14, 0xb5, 0x44, 0x8a, 0x37, 0x82, 0xe0, 0x28, 0x26, 0x3c, + 0x04, 0xba, 0x1f, 0xa1, 0x38, 0x75, 0xe3, 0xc0, 0xa8, 0x0e, 0xb4, 0x61, 0xc3, 0xa9, 0xf3, 0xf5, + 0x38, 0x80, 0xaf, 0x41, 0x3b, 0x4e, 0x63, 0x1a, 0xa3, 0xc4, 0x8d, 0x70, 0x1c, 0x46, 0xd4, 0xd8, + 0x1a, 0x68, 0xc3, 0x2d, 0xa7, 0x25, 0xd1, 0x4b, 0x0e, 0xc2, 0xb7, 0x60, 0x2f, 0x41, 0x84, 0xba, + 0x5e, 0x92, 0xf9, 0xdf, 0x14, 0xb3, 0xc6, 0x99, 0x1d, 0x56, 0xb0, 0x19, 0x2e, 0xb9, 0x0e, 0x68, + 0x95, 0xb8, 0x71, 0x60, 0x6c, 0xaf, 0x1b, 0x15, 0xe7, 0xe6, 0xbb, 0xc6, 0x17, 0x76, 0xef, 0xe1, + 0xa9, 0x5f, 0x99, 0x3f, 0xf5, 0x9b, 0xd7, 0x4a, 0x6a, 0x7c, 0xe1, 0x34, 0x17, 0xba, 0xe3, 0x00, + 0x5e, 0x83, 0x4e, 0x49, 0x93, 0x75, 0xdc, 0xd8, 0xe1, 0xaa, 0x47, 0x96, 0x18, 0x07, 0x4b, 0x8d, + 0x83, 0xf5, 0x59, 0x8d, 0x83, 0xad, 0x33, 0xd9, 0xfb, 0x3f, 0x7d, 0xcd, 0x69, 0x2d, 0xb4, 0x58, + 0x15, 0xda, 0x00, 0x2c, 0xba, 0x48, 0x8c, 0x06, 0x17, 0x32, 0xd7, 0xed, 0xdd, 0x28, 0xce, 0x04, + 0x53, 0xbb, 0x6a, 0x68, 0x4e, 0x69, 0x17, 0x3c, 0x07, 0x26, 0x77, 0x24, 0xb2, 0x70, 0x97, 0x15, + 0xd7, 0x8f, 0x50, 0x1a, 0xe2, 0xc0, 0x68, 0xf2, 0x78, 0xfe, 0x63, 0x2c, 0x91, 0xcc, 0x42, 0x8f, + 0x9c, 0x0b, 0x0a, 0x74, 0x40, 0xd7, 0xcf, 0x52, 0x82, 0x53, 0x32, 0x23, 0xae, 0x18, 0x18, 0x63, + 0x97, 0xdb, 0x39, 0x5e, 0xb7, 0x73, 0xae, 0x98, 0x9f, 0x38, 0xd1, 0xae, 0xb1, 0xe3, 0x39, 0x1d, + 0x7f, 0x15, 0x86, 0x1f, 0xc1, 0xab, 0xb2, 0xb1, 0x97, 0xfa, 0x0b, 0x7b, 0x2d, 0x6e, 0x6f, 0xb0, + 0xb4, 0xf7, 0x42, 0x5f, 0x79, 0x54, 0xad, 0x2f, 0x30, 0x99, 0x25, 0x94, 0xb8, 0x11, 0x22, 0x91, + 0xd1, 0x1e, 0x68, 0xc3, 0x5d, 0xd1, 0x7a, 0x47, 0xe0, 0x97, 0x88, 0x44, 0x6c, 0xd0, 0x50, 0x9e, + 0x0b, 0x4a, 0x87, 0x53, 0xea, 0x28, 0xcf, 0x79, 0xe9, 0x83, 0x94, 0x21, 0x34, 0x2b, 0xb0, 0x9a, + 0xa0, 0xee, 0x40, 0x1b, 0xd6, 0xec, 0xde, 0xfc, 0xa9, 0xdf, 0x61, 0xad, 0x9f, 0xb0, 0x9a, 0x30, + 0x23, 0xb4, 0x4b, 0x00, 0xec, 0x83, 0xa6, 0x87, 0xc8, 0x62, 0xeb, 0x1e, 0xdb, 0xea, 0x00, 0x06, + 0x49, 0xc2, 0x7b, 0xb0, 0x4b, 0xf0, 0xf7, 0x19, 0x4e, 0x7d, 0xcc, 0x3a, 0x66, 0x40, 0x1e, 0xe4, + 0xbe, 0x25, 0x6f, 0xd2, 0xa4, 0x54, 0x93, 0xd9, 0xad, 0xf0, 0xa1, 0x0d, 0xda, 0x45, 0x96, 0x24, + 0xec, 0x00, 0xb2, 0x15, 0x3d, 0xae, 0x70, 0xa0, 0x14, 0x1c, 0x51, 0x5d, 0x89, 0xbf, 0x55, 0x94, + 0x41, 0x38, 0x04, 0x5d, 0x19, 0x3e, 0x0a, 0x70, 0x21, 0x82, 0xd8, 0xe7, 0x41, 0xb4, 0x45, 0xd0, + 0x0c, 0xe6, 0x79, 0x9c, 0x82, 0x03, 0xce, 0x94, 0x83, 0x83, 0x03, 0x75, 0xb0, 0x03, 0xde, 0x97, + 0x1e, 0x2b, 0xde, 0xa8, 0x9a, 0x38, 0xe1, 0x55, 0x4d, 0xaf, 0x77, 0xf5, 0xab, 0x9a, 0xae, 0x77, + 0x1b, 0x57, 0x35, 0x1d, 0x74, 0x9b, 0x27, 0xef, 0x40, 0x6b, 0xc5, 0x15, 0x6c, 0x83, 0x6a, 0x80, + 0xf8, 0xd3, 0xd0, 0x70, 0xaa, 0x01, 0x82, 0xc6, 0xf2, 0xbd, 0x90, 0x37, 0x5f, 0x2e, 0xed, 0xcb, + 0x87, 0xb9, 0xa9, 0x3d, 0xce, 0x4d, 0xed, 0xef, 0xdc, 0xd4, 0xee, 0x9f, 0xcd, 0xca, 0xe3, 0xb3, + 0x59, 0xf9, 0xfd, 0x6c, 0x56, 0xbe, 0x58, 0x61, 0x4c, 0xa3, 0x99, 0x67, 0xf9, 0xd9, 0x94, 0x3d, + 0x43, 0x38, 0x65, 0xfc, 0x1f, 0x77, 0x3f, 0xd5, 0xd3, 0x24, 0xdf, 0x37, 0x4f, 0xae, 0xbd, 0x1d, + 0x7e, 0xf7, 0xce, 0xfe, 0x05, 0x00, 0x00, 0xff, 0xff, 0xd2, 0x4a, 0x7a, 0xa7, 0xd2, 0x05, 0x00, + 0x00, } func (m *State) Marshal() (dAtA []byte, err error) { @@ -333,6 +343,13 @@ func (m *State) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.LastValidatedHeight != 0 { + i = encodeVarintState(dAtA, i, uint64(m.LastValidatedHeight)) + i-- + dAtA[i] = 0x1 + i-- + dAtA[i] = 0xa8 + } if len(m.LastHeaderHash) > 0 { i -= len(m.LastHeaderHash) copy(dAtA[i:], m.LastHeaderHash) @@ -582,6 +599,9 @@ func (m *State) Size() (n int) { if l > 0 { n += 2 + l + sovState(uint64(l)) } + if m.LastValidatedHeight != 0 { + n += 2 + sovState(uint64(m.LastValidatedHeight)) + } return n } @@ -1122,6 +1142,25 @@ func (m *State) Unmarshal(dAtA []byte) error { m.LastHeaderHash = []byte{} } iNdEx = postIndex + case 21: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LastValidatedHeight", wireType) + } + m.LastValidatedHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowState + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.LastValidatedHeight |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipState(dAtA[iNdEx:]) diff --git a/types/serialization.go b/types/serialization.go index 1ccabd1ff..12cf30924 100644 --- a/types/serialization.go +++ b/types/serialization.go @@ -267,6 +267,8 @@ func (s *State) ToProto() (*pb.State, error) { LastResultsHash: s.LastResultsHash[:], AppHash: s.AppHash[:], RollappParams: s.RollappParams, + LastHeaderHash: s.LastHeaderHash[:], + LastValidatedHeight: int64(s.GetLastValidatedHeight()), }, nil } @@ -289,6 +291,8 @@ func (s *State) FromProto(other *pb.State) error { copy(s.LastResultsHash[:], other.LastResultsHash) copy(s.AppHash[:], other.AppHash) s.RollappParams = other.RollappParams + copy(s.LastHeaderHash[:], other.LastHeaderHash) + s.SetLastValidatedHeight(uint64(other.LastValidatedHeight)) return nil } diff --git a/types/state.go b/types/state.go index 1f4959a50..e583ca21d 100644 --- a/types/state.go +++ b/types/state.go @@ -45,6 +45,8 @@ type State struct { // LastHeaderHash is the hash of the last block header. LastHeaderHash [32]byte + // The last height which the node validated from available state updates. + LastValidatedHeight atomic.Uint64 } func (s *State) IsGenesis() bool { @@ -66,6 +68,22 @@ func (s *State) Height() uint64 { return s.LastBlockHeight.Load() } +// SetHeight sets the height saved in the Store if it is higher than the existing height +// returns OK if the value was updated successfully or did not need to be updated +func (s *State) SetLastValidatedHeight(height uint64) { + s.LastValidatedHeight.Store(max(s.GetLastValidatedHeight(), height)) +} + +// Height returns height of the highest block saved in the Store. +func (s *State) GetLastValidatedHeight() uint64 { + return s.LastValidatedHeight.Load() +} + +// Height returns height of the highest block saved in the Store. +func (s *State) NextValidationHeight() uint64 { + return s.LastValidatedHeight.Load() + 1 +} + // NextHeight returns the next height that expected to be stored in store. func (s *State) NextHeight() uint64 { if s.IsGenesis() {