diff --git a/docker-compose.yml b/docker-compose.yml index b1764592cf..07517ed3ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,8 @@ services: - ./docker/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro # The following line can be uncommented to persist metrics data. # - gossamer-prometheus:/prometheus + ports: + - 9090:9090/tcp # HTTP Web interface at http://localhost:9090/ expose: - 9090/tcp # Prometheus metrics for Grafana diff --git a/docker/grafana/provisioning/dashboards/gossamer.json b/docker/grafana/provisioning/dashboards/gossamer.json index 315f253aff..4c43d76cbc 100644 --- a/docker/grafana/provisioning/dashboards/gossamer.json +++ b/docker/grafana/provisioning/dashboards/gossamer.json @@ -155,65 +155,6 @@ "title": "Threads", "type": "stat" }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus_id" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "fixed" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 3, - "x": 6, - "y": 1 - }, - "id": 38, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "textMode": "auto" - }, - "pluginVersion": "9.0.4", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus_id" - }, - "expr": "process_resident_memory_bytes{instance=~\".*gossamer.*\"}", - "refId": "A" - } - ], - "title": "Memory usage", - "type": "stat" - }, { "datasource": { "type": "prometheus", @@ -309,6 +250,30 @@ "range": true, "refId": "B" }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_id" + }, + "editorMode": "code", + "expr": "go_memstats_heap_idle_bytes{instance=~\".*gossamer.*\"}", + "hide": false, + "legendFormat": "Heap Idle Bytes", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_id" + }, + "editorMode": "code", + "expr": "go_memstats_heap_released_bytes{instance=~\".*gossamer.*\"}", + "hide": false, + "legendFormat": "Heap Idle Bytes", + "range": true, + "refId": "D" + }, { "datasource": { "type": "prometheus", @@ -319,12 +284,103 @@ "hide": false, "legendFormat": "Stack", "range": true, - "refId": "C" + "refId": "E" } ], "title": "Memory usage", "type": "timeseries" }, + { + "datasource": { + "uid": "prometheus_id", + "type": "prometheus" + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "line", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 0, + "gradientMode": "none", + "spanNulls": false, + "showPoints": "auto", + "pointSize": 5, + "stacking": { + "mode": "none", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "scaleDistribution": { + "type": "linear" + }, + "axisCenteredZero": false, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 40, + "options": { + "tooltip": { + "mode": "multi", + "sort": "none" + }, + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom", + "calcs": [] + } + }, + "pluginVersion": "9.0.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus_id" + }, + "refId": "A", + "editorMode": "builder", + "expr": "gossamer_inmemory_runtimes_total{instance=~\".*gossamer.*\"}", + "legendFormat": "Runtimes", + "range": true + } + ], + "title": "In Memory Runtimes", + "type": "timeseries", + "description": "" + }, { "gridPos": { "h": 1, diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml index 88d165df8a..22aa7ef03c 100644 --- a/docker/prometheus/prometheus.yml +++ b/docker/prometheus/prometheus.yml @@ -10,4 +10,6 @@ scrape_configs: - job_name: gossamer metrics_path: /metrics static_configs: + # for mac users, change to host.docker.internal:9876 if you want to + # communicate prometheus container with gossamer running locally - targets: ["gossamer:9876"] diff --git a/dot/core/service.go b/dot/core/service.go index 6ab24182e4..4a2aaa8b65 100644 --- a/dot/core/service.go +++ b/dot/core/service.go @@ -212,13 +212,14 @@ func (s *Service) handleBlock(block *types.Block, state *rtstorage.TrieState) er logger.Debugf("imported block %s and stored state trie with root %s", block.Header.Hash(), state.MustRoot()) - rt, err := s.blockState.GetRuntime(block.Header.ParentHash) + parentRuntimeInstance, err := s.blockState.GetRuntime(block.Header.ParentHash) if err != nil { return err } // check for runtime changes - if err := s.blockState.HandleRuntimeChanges(state, rt, block.Header.Hash()); err != nil { + err = s.blockState.HandleRuntimeChanges(state, parentRuntimeInstance, block.Header.Hash()) + if err != nil { logger.Criticalf("failed to update runtime code: %s", err) return err } diff --git a/dot/core/service_integration_test.go b/dot/core/service_integration_test.go index ebbc3ea28e..00956528fd 100644 --- a/dot/core/service_integration_test.go +++ b/dot/core/service_integration_test.go @@ -497,86 +497,111 @@ func TestService_GetMetadata(t *testing.T) { } func TestService_HandleRuntimeChanges(t *testing.T) { - const ( - updatedSpecVersion = uint32(262) - updateNodeRuntimeWasmPath = "../../tests/polkadotjs_test/test/node_runtime.compact.wasm" - ) s := NewTestService(t, nil) + genesisHeader, err := s.blockState.BestBlockHeader() + require.NoError(t, err) - bestBlockHash := s.blockState.BestBlockHash() - rt, err := s.blockState.GetRuntime(bestBlockHash) + genesisBlockHash := genesisHeader.Hash() + genesisStateRoot := genesisHeader.StateRoot + + ts, err := s.storageState.TrieState(&genesisStateRoot) // Pass genesis root require.NoError(t, err) - v, err := rt.Version() + firstBlockHash := createBlockUsingOldRuntime(t, genesisBlockHash, ts, s.blockState) + updateNodeRuntimeWasmPath, err := runtime.GetRuntime(context.Background(), runtime.WESTEND_RUNTIME_v0929) require.NoError(t, err) - currSpecVersion := v.SpecVersion // genesis runtime version. - hash := s.blockState.BestBlockHash() // genesisHash + secondBlockHash := createBlockUsingNewRuntime(t, genesisBlockHash, updateNodeRuntimeWasmPath, ts, s.blockState) - digest := types.NewDigest() - err = digest.Add(types.PreRuntimeDigest{ - ConsensusEngineID: types.BabeEngineID, - Data: common.MustHexToBytes("0x0201000000ef55a50f00000000"), - }) + // firstBlockHash runtime should not be updated + genesisRuntime, err := s.blockState.GetRuntime(genesisBlockHash) require.NoError(t, err) - newBlock1 := &types.Block{ - Header: types.Header{ - ParentHash: hash, - Number: 1, - Digest: types.NewDigest()}, - Body: *types.NewBody([]types.Extrinsic{[]byte("Old Runtime")}), - } + firstBlockRuntime, err := s.blockState.GetRuntime(firstBlockHash) + require.NoError(t, err) - newBlockRTUpdate := &types.Block{ - Header: types.Header{ - ParentHash: hash, - Number: 1, - Digest: digest, - }, - Body: *types.NewBody([]types.Extrinsic{[]byte("Updated Runtime")}), - } + genesisRuntimeVersion, err := genesisRuntime.Version() + require.NoError(t, err) - ts, err := s.storageState.TrieState(nil) // Pass genesis root + firstBlockRuntimeVersion, err := firstBlockRuntime.Version() require.NoError(t, err) - parentRt, err := s.blockState.GetRuntime(hash) + require.Equal(t, genesisRuntimeVersion, firstBlockRuntimeVersion) + + secondBlockRuntime, err := s.blockState.GetRuntime(secondBlockHash) require.NoError(t, err) - v, err = parentRt.Version() + const updatedSpecVersion = uint32(9290) + secondBlockRuntimeVersion, err := secondBlockRuntime.Version() require.NoError(t, err) - require.Equal(t, v.SpecVersion, currSpecVersion) + require.Equal(t, updatedSpecVersion, secondBlockRuntimeVersion.SpecVersion) +} - bhash1 := newBlock1.Header.Hash() - err = s.blockState.HandleRuntimeChanges(ts, parentRt, bhash1) +func createBlockUsingOldRuntime(t *testing.T, bestBlockHash common.Hash, trieState *rtstorage.TrieState, + blockState BlockState) (blockHash common.Hash) { + parentRt, err := blockState.GetRuntime(bestBlockHash) require.NoError(t, err) - testRuntime, err := os.ReadFile(updateNodeRuntimeWasmPath) + primaryDigestData := types.NewBabePrimaryPreDigest(0, uint64(0), [32]byte{}, [64]byte{}) + digest := types.NewDigest() + preRuntimeDigest, err := primaryDigestData.ToPreRuntimeDigest() + require.NoError(t, err) + err = digest.Add(*preRuntimeDigest) require.NoError(t, err) - ts.Put(common.CodeKey, testRuntime) - rtUpdateBhash := newBlockRTUpdate.Header.Hash() + newBlock := &types.Block{ + Header: types.Header{ + ParentHash: bestBlockHash, + Number: 1, + Digest: digest, + }, + Body: *types.NewBody([]types.Extrinsic{[]byte("Old Runtime")}), + } + err = blockState.AddBlock(newBlock) + require.NoError(t, err) - // update runtime for new block - err = s.blockState.HandleRuntimeChanges(ts, parentRt, rtUpdateBhash) + newBlockHash := newBlock.Header.Hash() + err = blockState.HandleRuntimeChanges(trieState, parentRt, newBlockHash) require.NoError(t, err) - // bhash1 runtime should not be updated - rt, err = s.blockState.GetRuntime(bhash1) + return newBlockHash +} + +func createBlockUsingNewRuntime(t *testing.T, bestBlockHash common.Hash, newRuntimePath string, + trieState *rtstorage.TrieState, blockState BlockState) (blockHash common.Hash) { + parentRt, err := blockState.GetRuntime(bestBlockHash) require.NoError(t, err) - v, err = rt.Version() + testRuntime, err := os.ReadFile(newRuntimePath) require.NoError(t, err) - require.Equal(t, v.SpecVersion, currSpecVersion) - rt, err = s.blockState.GetRuntime(rtUpdateBhash) + trieState.Put(common.CodeKey, testRuntime) + + primaryDigestData := types.NewBabePrimaryPreDigest(0, uint64(1), [32]byte{}, [64]byte{}) + digest := types.NewDigest() + preRuntimeDigest, err := primaryDigestData.ToPreRuntimeDigest() + require.NoError(t, err) + err = digest.Add(*preRuntimeDigest) + require.NoError(t, err) + + newBlockRuntimeUpdate := &types.Block{ + Header: types.Header{ + ParentHash: bestBlockHash, + Number: 1, + Digest: digest, + }, + Body: *types.NewBody([]types.Extrinsic{[]byte("Updated Runtime")}), + } + + err = blockState.AddBlock(newBlockRuntimeUpdate) require.NoError(t, err) - v, err = rt.Version() + newBlockRTUpdateHash := newBlockRuntimeUpdate.Header.Hash() + err = blockState.HandleRuntimeChanges(trieState, parentRt, newBlockRTUpdateHash) require.NoError(t, err) - require.Equal(t, v.SpecVersion, updatedSpecVersion) + return newBlockRTUpdateHash } func TestService_HandleCodeSubstitutes(t *testing.T) { diff --git a/dot/state/block.go b/dot/state/block.go index f1002c4555..fbcbe5d4c8 100644 --- a/dot/state/block.go +++ b/dot/state/block.go @@ -828,20 +828,23 @@ func (bs *BlockState) setArrivalTime(hash common.Hash, arrivalTime time.Time) er // HandleRuntimeChanges handles the update in runtime. func (bs *BlockState) HandleRuntimeChanges(newState *rtstorage.TrieState, - rt runtime.Instance, bHash common.Hash) error { + parentRuntimeInstance runtime.Instance, bHash common.Hash) error { currCodeHash, err := newState.LoadCodeHash() if err != nil { return err } - codeHash := rt.GetCodeHash() - if bytes.Equal(codeHash[:], currCodeHash[:]) { - bs.StoreRuntime(bHash, rt) + parentCodeHash := parentRuntimeInstance.GetCodeHash() + + // if the parent code hash is the same as the new code hash + // we do nothing since we don't want to store duplicate runtimes + // for different hashes + if bytes.Equal(parentCodeHash[:], currCodeHash[:]) { return nil } logger.Infof("🔄 detected runtime code change, upgrading with block %s from previous code hash %s to new code hash %s...", //nolint:lll - bHash, codeHash, currCodeHash) + bHash, parentCodeHash, currCodeHash) code := newState.LoadCode() if len(code) == 0 { return errors.New("new :code is empty") @@ -856,30 +859,31 @@ func (bs *BlockState) HandleRuntimeChanges(newState *rtstorage.TrieState, } // only update runtime during code substitution if runtime SpecVersion is updated - previousVersion, err := rt.Version() + previousVersion, err := parentRuntimeInstance.Version() if err != nil { return err } + if previousVersion.SpecVersion == newVersion.SpecVersion { logger.Info("not upgrading runtime code during code substitution") - bs.StoreRuntime(bHash, rt) + bs.StoreRuntime(bHash, parentRuntimeInstance) return nil } logger.Infof( "🔄 detected runtime code change, upgrading with block %s from previous code hash %s and spec %d to new code hash %s and spec %d...", //nolint:lll - bHash, codeHash, previousVersion.SpecVersion, currCodeHash, newVersion.SpecVersion) + bHash, parentCodeHash, previousVersion.SpecVersion, currCodeHash, newVersion.SpecVersion) } rtCfg := wasmer.Config{ Storage: newState, - Keystore: rt.Keystore(), - NodeStorage: rt.NodeStorage(), - Network: rt.NetworkService(), + Keystore: parentRuntimeInstance.Keystore(), + NodeStorage: parentRuntimeInstance.NodeStorage(), + Network: parentRuntimeInstance.NetworkService(), CodeHash: currCodeHash, } - if rt.Validator() { + if parentRuntimeInstance.Validator() { rtCfg.Role = 4 } @@ -895,7 +899,7 @@ func (bs *BlockState) HandleRuntimeChanges(newState *rtstorage.TrieState, return fmt.Errorf("failed to update code substituted block hash: %w", err) } - newVersion, err := rt.Version() + newVersion, err := parentRuntimeInstance.Version() if err != nil { return err } @@ -905,7 +909,23 @@ func (bs *BlockState) HandleRuntimeChanges(newState *rtstorage.TrieState, // GetRuntime gets the runtime instance pointer for the block hash given. func (bs *BlockState) GetRuntime(blockHash common.Hash) (instance runtime.Instance, err error) { - return bs.bt.GetBlockRuntime(blockHash) + // we search primarily in the blocktree so we ensure the + // fork aware property while searching for a runtime, however + // if there is no runtimes in that fork then we look for the + // closest ancestor with a runtime instance + runtimeInstance, err := bs.bt.GetBlockRuntime(blockHash) + + if err != nil { + // in this case the node is not in the blocktree which mean + // it is a finalized node already persisted in database + if errors.Is(err, blocktree.ErrNodeNotFound) { + panic(err.Error() + " see https://github.com/ChainSafe/gossamer/issues/3066") + } + + return nil, fmt.Errorf("while getting runtime: %w", err) + } + + return runtimeInstance, nil } // StoreRuntime stores the runtime for corresponding block hash. diff --git a/dot/state/block_test.go b/dot/state/block_test.go index af05b5c9c3..f4b9cf7f23 100644 --- a/dot/state/block_test.go +++ b/dot/state/block_test.go @@ -1079,3 +1079,43 @@ func Test_loadHeaderFromDisk_WithGenesisBlock(t *testing.T) { require.NoError(t, err) require.Equal(t, genesisHeader.Hash(), header.Hash()) } + +func Test_GetRuntime_StoreRuntime(t *testing.T) { + ctrl := gomock.NewController(t) + + telemetryMock := NewMockTelemetry(ctrl) + telemetryMock.EXPECT().SendMessage(gomock.Any()).AnyTimes() + + db := NewInMemoryDB(t) + + genesisHeader := &types.Header{ + Number: 0, + StateRoot: trie.EmptyHash, + Digest: types.NewDigest(), + } + genesisHash := genesisHeader.Hash() + blockState, err := NewBlockStateFromGenesis(db, newTriesEmpty(), genesisHeader, telemetryMock) + require.NoError(t, err) + + runtimeInstance := NewMockInstance(nil) + blockState.StoreRuntime(genesisHash, runtimeInstance) + + genesisRuntimeInstance, err := blockState.GetRuntime(genesisHash) + require.NoError(t, err) + require.Equal(t, runtimeInstance, genesisRuntimeInstance) + + chain, _ := AddBlocksToState(t, blockState, 5, false) + for _, hashInChain := range chain { + genesisRuntimeInstance, err := blockState.GetRuntime(hashInChain.Hash()) + require.NoError(t, err) + require.Equal(t, runtimeInstance, genesisRuntimeInstance) + } + + lastElementOnChain := chain[len(chain)-1] + err = blockState.SetFinalisedHash(lastElementOnChain.Hash(), 1, 0) + require.NoError(t, err) + + sameRuntimeOnDiffHash, err := blockState.GetRuntime(lastElementOnChain.Hash()) + require.NoError(t, err) + require.Equal(t, runtimeInstance, sameRuntimeOnDiffHash) +} diff --git a/dot/state/mocks_generate_test.go b/dot/state/mocks_generate_test.go index ee844e6fa2..67cca78fd7 100644 --- a/dot/state/mocks_generate_test.go +++ b/dot/state/mocks_generate_test.go @@ -4,5 +4,6 @@ package state //go:generate mockgen -destination=mocks_test.go -package $GOPACKAGE . Telemetry,BlockStateDatabase,Observer +//go:generate mockgen -destination=mocks_runtime_test.go -package $GOPACKAGE github.com/ChainSafe/gossamer/lib/runtime Instance //go:generate mockgen -destination=mock_gauge_test.go -package $GOPACKAGE github.com/prometheus/client_golang/prometheus Gauge //go:generate mockgen -destination=mock_counter_test.go -package $GOPACKAGE github.com/prometheus/client_golang/prometheus Counter diff --git a/dot/state/mocks_runtime_test.go b/dot/state/mocks_runtime_test.go new file mode 100644 index 0000000000..83be8da0e1 --- /dev/null +++ b/dot/state/mocks_runtime_test.go @@ -0,0 +1,434 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ChainSafe/gossamer/lib/runtime (interfaces: Instance) + +// Package state is a generated GoMock package. +package state + +import ( + reflect "reflect" + + types "github.com/ChainSafe/gossamer/dot/types" + common "github.com/ChainSafe/gossamer/lib/common" + ed25519 "github.com/ChainSafe/gossamer/lib/crypto/ed25519" + keystore "github.com/ChainSafe/gossamer/lib/keystore" + runtime "github.com/ChainSafe/gossamer/lib/runtime" + transaction "github.com/ChainSafe/gossamer/lib/transaction" + gomock "github.com/golang/mock/gomock" +) + +// MockInstance is a mock of Instance interface. +type MockInstance struct { + ctrl *gomock.Controller + recorder *MockInstanceMockRecorder +} + +// MockInstanceMockRecorder is the mock recorder for MockInstance. +type MockInstanceMockRecorder struct { + mock *MockInstance +} + +// NewMockInstance creates a new mock instance. +func NewMockInstance(ctrl *gomock.Controller) *MockInstance { + mock := &MockInstance{ctrl: ctrl} + mock.recorder = &MockInstanceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInstance) EXPECT() *MockInstanceMockRecorder { + return m.recorder +} + +// ApplyExtrinsic mocks base method. +func (m *MockInstance) ApplyExtrinsic(arg0 types.Extrinsic) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ApplyExtrinsic", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ApplyExtrinsic indicates an expected call of ApplyExtrinsic. +func (mr *MockInstanceMockRecorder) ApplyExtrinsic(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyExtrinsic", reflect.TypeOf((*MockInstance)(nil).ApplyExtrinsic), arg0) +} + +// BabeConfiguration mocks base method. +func (m *MockInstance) BabeConfiguration() (*types.BabeConfiguration, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BabeConfiguration") + ret0, _ := ret[0].(*types.BabeConfiguration) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BabeConfiguration indicates an expected call of BabeConfiguration. +func (mr *MockInstanceMockRecorder) BabeConfiguration() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BabeConfiguration", reflect.TypeOf((*MockInstance)(nil).BabeConfiguration)) +} + +// BabeGenerateKeyOwnershipProof mocks base method. +func (m *MockInstance) BabeGenerateKeyOwnershipProof(arg0 uint64, arg1 [32]byte) (types.OpaqueKeyOwnershipProof, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BabeGenerateKeyOwnershipProof", arg0, arg1) + ret0, _ := ret[0].(types.OpaqueKeyOwnershipProof) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BabeGenerateKeyOwnershipProof indicates an expected call of BabeGenerateKeyOwnershipProof. +func (mr *MockInstanceMockRecorder) BabeGenerateKeyOwnershipProof(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BabeGenerateKeyOwnershipProof", reflect.TypeOf((*MockInstance)(nil).BabeGenerateKeyOwnershipProof), arg0, arg1) +} + +// BabeSubmitReportEquivocationUnsignedExtrinsic mocks base method. +func (m *MockInstance) BabeSubmitReportEquivocationUnsignedExtrinsic(arg0 types.BabeEquivocationProof, arg1 types.OpaqueKeyOwnershipProof) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BabeSubmitReportEquivocationUnsignedExtrinsic", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// BabeSubmitReportEquivocationUnsignedExtrinsic indicates an expected call of BabeSubmitReportEquivocationUnsignedExtrinsic. +func (mr *MockInstanceMockRecorder) BabeSubmitReportEquivocationUnsignedExtrinsic(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BabeSubmitReportEquivocationUnsignedExtrinsic", reflect.TypeOf((*MockInstance)(nil).BabeSubmitReportEquivocationUnsignedExtrinsic), arg0, arg1) +} + +// CheckInherents mocks base method. +func (m *MockInstance) CheckInherents() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "CheckInherents") +} + +// CheckInherents indicates an expected call of CheckInherents. +func (mr *MockInstanceMockRecorder) CheckInherents() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckInherents", reflect.TypeOf((*MockInstance)(nil).CheckInherents)) +} + +// DecodeSessionKeys mocks base method. +func (m *MockInstance) DecodeSessionKeys(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DecodeSessionKeys", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DecodeSessionKeys indicates an expected call of DecodeSessionKeys. +func (mr *MockInstanceMockRecorder) DecodeSessionKeys(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeSessionKeys", reflect.TypeOf((*MockInstance)(nil).DecodeSessionKeys), arg0) +} + +// Exec mocks base method. +func (m *MockInstance) Exec(arg0 string, arg1 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Exec", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Exec indicates an expected call of Exec. +func (mr *MockInstanceMockRecorder) Exec(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockInstance)(nil).Exec), arg0, arg1) +} + +// ExecuteBlock mocks base method. +func (m *MockInstance) ExecuteBlock(arg0 *types.Block) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteBlock", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteBlock indicates an expected call of ExecuteBlock. +func (mr *MockInstanceMockRecorder) ExecuteBlock(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBlock", reflect.TypeOf((*MockInstance)(nil).ExecuteBlock), arg0) +} + +// FinalizeBlock mocks base method. +func (m *MockInstance) FinalizeBlock() (*types.Header, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FinalizeBlock") + ret0, _ := ret[0].(*types.Header) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FinalizeBlock indicates an expected call of FinalizeBlock. +func (mr *MockInstanceMockRecorder) FinalizeBlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeBlock", reflect.TypeOf((*MockInstance)(nil).FinalizeBlock)) +} + +// GenerateSessionKeys mocks base method. +func (m *MockInstance) GenerateSessionKeys() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "GenerateSessionKeys") +} + +// GenerateSessionKeys indicates an expected call of GenerateSessionKeys. +func (mr *MockInstanceMockRecorder) GenerateSessionKeys() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateSessionKeys", reflect.TypeOf((*MockInstance)(nil).GenerateSessionKeys)) +} + +// GetCodeHash mocks base method. +func (m *MockInstance) GetCodeHash() common.Hash { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCodeHash") + ret0, _ := ret[0].(common.Hash) + return ret0 +} + +// GetCodeHash indicates an expected call of GetCodeHash. +func (mr *MockInstanceMockRecorder) GetCodeHash() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCodeHash", reflect.TypeOf((*MockInstance)(nil).GetCodeHash)) +} + +// GrandpaAuthorities mocks base method. +func (m *MockInstance) GrandpaAuthorities() ([]types.Authority, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrandpaAuthorities") + ret0, _ := ret[0].([]types.Authority) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GrandpaAuthorities indicates an expected call of GrandpaAuthorities. +func (mr *MockInstanceMockRecorder) GrandpaAuthorities() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrandpaAuthorities", reflect.TypeOf((*MockInstance)(nil).GrandpaAuthorities)) +} + +// GrandpaGenerateKeyOwnershipProof mocks base method. +func (m *MockInstance) GrandpaGenerateKeyOwnershipProof(arg0 uint64, arg1 ed25519.PublicKeyBytes) (types.GrandpaOpaqueKeyOwnershipProof, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrandpaGenerateKeyOwnershipProof", arg0, arg1) + ret0, _ := ret[0].(types.GrandpaOpaqueKeyOwnershipProof) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GrandpaGenerateKeyOwnershipProof indicates an expected call of GrandpaGenerateKeyOwnershipProof. +func (mr *MockInstanceMockRecorder) GrandpaGenerateKeyOwnershipProof(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrandpaGenerateKeyOwnershipProof", reflect.TypeOf((*MockInstance)(nil).GrandpaGenerateKeyOwnershipProof), arg0, arg1) +} + +// GrandpaSubmitReportEquivocationUnsignedExtrinsic mocks base method. +func (m *MockInstance) GrandpaSubmitReportEquivocationUnsignedExtrinsic(arg0 types.GrandpaEquivocationProof, arg1 types.GrandpaOpaqueKeyOwnershipProof) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GrandpaSubmitReportEquivocationUnsignedExtrinsic", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// GrandpaSubmitReportEquivocationUnsignedExtrinsic indicates an expected call of GrandpaSubmitReportEquivocationUnsignedExtrinsic. +func (mr *MockInstanceMockRecorder) GrandpaSubmitReportEquivocationUnsignedExtrinsic(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GrandpaSubmitReportEquivocationUnsignedExtrinsic", reflect.TypeOf((*MockInstance)(nil).GrandpaSubmitReportEquivocationUnsignedExtrinsic), arg0, arg1) +} + +// InherentExtrinsics mocks base method. +func (m *MockInstance) InherentExtrinsics(arg0 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InherentExtrinsics", arg0) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InherentExtrinsics indicates an expected call of InherentExtrinsics. +func (mr *MockInstanceMockRecorder) InherentExtrinsics(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InherentExtrinsics", reflect.TypeOf((*MockInstance)(nil).InherentExtrinsics), arg0) +} + +// InitializeBlock mocks base method. +func (m *MockInstance) InitializeBlock(arg0 *types.Header) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InitializeBlock", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// InitializeBlock indicates an expected call of InitializeBlock. +func (mr *MockInstanceMockRecorder) InitializeBlock(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InitializeBlock", reflect.TypeOf((*MockInstance)(nil).InitializeBlock), arg0) +} + +// Keystore mocks base method. +func (m *MockInstance) Keystore() *keystore.GlobalKeystore { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Keystore") + ret0, _ := ret[0].(*keystore.GlobalKeystore) + return ret0 +} + +// Keystore indicates an expected call of Keystore. +func (mr *MockInstanceMockRecorder) Keystore() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Keystore", reflect.TypeOf((*MockInstance)(nil).Keystore)) +} + +// Metadata mocks base method. +func (m *MockInstance) Metadata() ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Metadata") + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Metadata indicates an expected call of Metadata. +func (mr *MockInstanceMockRecorder) Metadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockInstance)(nil).Metadata)) +} + +// NetworkService mocks base method. +func (m *MockInstance) NetworkService() runtime.BasicNetwork { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NetworkService") + ret0, _ := ret[0].(runtime.BasicNetwork) + return ret0 +} + +// NetworkService indicates an expected call of NetworkService. +func (mr *MockInstanceMockRecorder) NetworkService() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkService", reflect.TypeOf((*MockInstance)(nil).NetworkService)) +} + +// NodeStorage mocks base method. +func (m *MockInstance) NodeStorage() runtime.NodeStorage { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NodeStorage") + ret0, _ := ret[0].(runtime.NodeStorage) + return ret0 +} + +// NodeStorage indicates an expected call of NodeStorage. +func (mr *MockInstanceMockRecorder) NodeStorage() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NodeStorage", reflect.TypeOf((*MockInstance)(nil).NodeStorage)) +} + +// OffchainWorker mocks base method. +func (m *MockInstance) OffchainWorker() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "OffchainWorker") +} + +// OffchainWorker indicates an expected call of OffchainWorker. +func (mr *MockInstanceMockRecorder) OffchainWorker() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OffchainWorker", reflect.TypeOf((*MockInstance)(nil).OffchainWorker)) +} + +// PaymentQueryInfo mocks base method. +func (m *MockInstance) PaymentQueryInfo(arg0 []byte) (*types.RuntimeDispatchInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentQueryInfo", arg0) + ret0, _ := ret[0].(*types.RuntimeDispatchInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentQueryInfo indicates an expected call of PaymentQueryInfo. +func (mr *MockInstanceMockRecorder) PaymentQueryInfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentQueryInfo", reflect.TypeOf((*MockInstance)(nil).PaymentQueryInfo), arg0) +} + +// RandomSeed mocks base method. +func (m *MockInstance) RandomSeed() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "RandomSeed") +} + +// RandomSeed indicates an expected call of RandomSeed. +func (mr *MockInstanceMockRecorder) RandomSeed() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RandomSeed", reflect.TypeOf((*MockInstance)(nil).RandomSeed)) +} + +// SetContextStorage mocks base method. +func (m *MockInstance) SetContextStorage(arg0 runtime.Storage) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetContextStorage", arg0) +} + +// SetContextStorage indicates an expected call of SetContextStorage. +func (mr *MockInstanceMockRecorder) SetContextStorage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetContextStorage", reflect.TypeOf((*MockInstance)(nil).SetContextStorage), arg0) +} + +// Stop mocks base method. +func (m *MockInstance) Stop() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Stop") +} + +// Stop indicates an expected call of Stop. +func (mr *MockInstanceMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockInstance)(nil).Stop)) +} + +// ValidateTransaction mocks base method. +func (m *MockInstance) ValidateTransaction(arg0 types.Extrinsic) (*transaction.Validity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateTransaction", arg0) + ret0, _ := ret[0].(*transaction.Validity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateTransaction indicates an expected call of ValidateTransaction. +func (mr *MockInstanceMockRecorder) ValidateTransaction(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateTransaction", reflect.TypeOf((*MockInstance)(nil).ValidateTransaction), arg0) +} + +// Validator mocks base method. +func (m *MockInstance) Validator() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validator") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Validator indicates an expected call of Validator. +func (mr *MockInstanceMockRecorder) Validator() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validator", reflect.TypeOf((*MockInstance)(nil).Validator)) +} + +// Version mocks base method. +func (m *MockInstance) Version() (runtime.Version, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Version") + ret0, _ := ret[0].(runtime.Version) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Version indicates an expected call of Version. +func (mr *MockInstanceMockRecorder) Version() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Version", reflect.TypeOf((*MockInstance)(nil).Version)) +} diff --git a/lib/blocktree/blocktree.go b/lib/blocktree/blocktree.go index 27d45f8793..0f1faf21cc 100644 --- a/lib/blocktree/blocktree.go +++ b/lib/blocktree/blocktree.go @@ -10,6 +10,7 @@ import ( "time" "github.com/ChainSafe/gossamer/dot/types" + "github.com/ChainSafe/gossamer/internal/log" "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/runtime" @@ -19,13 +20,20 @@ import ( "golang.org/x/exp/maps" ) +var logger = log.NewFromGlobal(log.AddContext("pkg", "blocktree")) var ( leavesGauge = promauto.NewGauge(prometheus.GaugeOpts{ Namespace: "gossamer_block", Name: "leaves_total", Help: "total number of blocktree leaves", }) + inMemoryRuntimesGauge = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: "gossamer_blocktree", + Name: "runtimes_total", + Help: "total number of runtimes stored in the in-memory blocktree", + }) errAncestorOutOfBoundsCheck = errors.New("out of bounds ancestor check") + ErrRuntimeNotFound = errors.New("runtime not found") ) // Hash common.Hash @@ -261,6 +269,25 @@ func (bt *BlockTree) Prune(finalised Hash) (pruned []Hash) { return pruned } + // Cleanup in-memory runtimes from the canonical chain. + // The runtime used in the newly finalised block is kept + // instantiated in memory, and all other runtimes are + // stopped and removed from memory. Note these are still + // accessible through the storage as WASM blob. + previousFinalisedBlock := bt.root + newCanonicalChainBlocksCount := n.number - previousFinalisedBlock.number + if previousFinalisedBlock.number == 0 { // include the genesis block + newCanonicalChainBlocksCount++ + } + canonicalChainBlock := n + newCanonicalChainBlockHashes := make([]common.Hash, newCanonicalChainBlocksCount) + for i := int(newCanonicalChainBlocksCount) - 1; i >= 0; i-- { + newCanonicalChainBlockHashes[i] = canonicalChainBlock.hash + canonicalChainBlock = canonicalChainBlock.parent + } + + bt.runtimes.onFinalisation(newCanonicalChainBlockHashes) + pruned = bt.root.prune(n, nil) bt.root = n bt.root.parent = nil @@ -271,10 +298,6 @@ func (bt *BlockTree) Prune(finalised Hash) (pruned []Hash) { bt.leaves.store(leaf.hash, leaf) } - for _, hash := range pruned { - bt.runtimes.delete(hash) - } - leavesGauge.Set(float64(len(bt.leaves.nodes()))) return pruned } @@ -294,7 +317,7 @@ func (bt *BlockTree) String() string { // Format leaves var leaves string - bt.leaves.smap.Range(func(hash, node interface{}) bool { + bt.leaves.smap.Range(func(hash, _ interface{}) bool { leaves = leaves + fmt.Sprintf("%s\n", hash.(Hash)) return true }) @@ -505,15 +528,38 @@ func (bt *BlockTree) DeepCopy() *BlockTree { } // StoreRuntime stores the runtime for corresponding block hash. -func (bt *BlockTree) StoreRuntime(hash common.Hash, in runtime.Instance) { - bt.runtimes.set(hash, in) +func (bt *BlockTree) StoreRuntime(hash common.Hash, instance runtime.Instance) { + bt.runtimes.set(hash, instance) } -// GetBlockRuntime returns block runtime for corresponding block hash. +// GetBlockRuntime returns the runtime corresponding to the given block hash. If there is no instance for +// the given block hash it will lookup an instance of an ancestor and return it. func (bt *BlockTree) GetBlockRuntime(hash common.Hash) (runtime.Instance, error) { - ins := bt.runtimes.get(hash) - if ins == nil { - return nil, fmt.Errorf("%w for hash %s", ErrFailedToGetRuntime, hash) + // if the current node contains a runtime entry in the runtime mapping + // then we early return the instance, otherwise we will lookup for the + // closest parent with a runtime instance entry in the mapping + runtimeInstance := bt.runtimes.get(hash) + if runtimeInstance != nil { + return runtimeInstance, nil + } + + bt.RLock() + defer bt.RUnlock() + + currentNode := bt.getNode(hash) + if currentNode == nil { + return nil, fmt.Errorf("%w: for block hash %s", ErrNodeNotFound, hash) } - return ins, nil + + currentNode = currentNode.parent + for currentNode != nil { + runtimeInstance := bt.runtimes.get(currentNode.hash) + if runtimeInstance != nil { + return runtimeInstance, nil + } + + currentNode = currentNode.parent + } + + return nil, nil } diff --git a/lib/blocktree/blocktree_test.go b/lib/blocktree/blocktree_test.go index 6b479d9130..798ec32bb7 100644 --- a/lib/blocktree/blocktree_test.go +++ b/lib/blocktree/blocktree_test.go @@ -11,6 +11,8 @@ import ( "github.com/ChainSafe/gossamer/dot/types" "github.com/ChainSafe/gossamer/lib/common" + "github.com/ChainSafe/gossamer/lib/runtime" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -381,41 +383,399 @@ func Test_BlockTree_LowestCommonAncestor_SameChain(t *testing.T) { require.Equal(t, b, p) } -func Test_BlockTree_Prune(t *testing.T) { - var bt *BlockTree - var branches []testBranch +func buildLinearBlockTree(t *testing.T, amount int) *BlockTree { + t.Helper() - for { - bt, branches = createTestBlockTree(t, testHeader, 5) - if len(branches) > 0 && len(bt.getNode(branches[0].hash).children) > 1 { - break - } + blockTree := &BlockTree{ + leaves: newEmptyLeafMap(), + runtimes: newHashToRuntime(), } - blockTreeCopy := bt.DeepCopy() + rootNode := &node{ + hash: common.MustHexToHash("0x00"), + number: 0, + } - // pick some block to finalise - finalised := bt.root.children[0].children[0].children[0] - pruned := bt.Prune(finalised.hash) + blockTree.root = rootNode - for _, prunedHash := range pruned { - prunedNode := blockTreeCopy.getNode(prunedHash) - if prunedNode.isDescendantOf(finalised) { - t.Fatal("pruned node that's descendant of finalised node!!") + parentNode := rootNode + for idx := 1; idx < amount; idx++ { + newNode := &node{ + parent: parentNode, + hash: common.MustHexToHash(fmt.Sprintf("0x0%d", idx)), + number: uint(idx), } - if finalised.isDescendantOf(prunedNode) { - t.Fatal("pruned an ancestor of the finalised node!!") + parentNode.addChild(newNode) + parentNode = newNode + } + + // parentNode node here will be the latest block in the tree + // that means it will be the leaf as well + blockTree.leaves.store(parentNode.hash, parentNode) + return blockTree +} + +func appendRuntimeToHash(t *testing.T, blockTree *BlockTree, + hash common.Hash, runtimeInstance runtime.Instance) { + t.Helper() + + blockTree.runtimes.set(hash, runtimeInstance) +} + +func appendForksAt(t *testing.T, blockTree *BlockTree, forkAt common.Hash, forkHashes ...common.Hash) { + t.Helper() + + parentNode := blockTree.getNode(forkAt) + require.NotNil(t, parentNode) + + for idx, hash := range forkHashes { + newNode := &node{ + parent: parentNode, + hash: hash, + number: uint(100 + idx), } + parentNode.addChild(newNode) + parentNode = newNode } - require.NotEqual(t, 0, len(bt.leaves.nodes())) - for _, leaf := range bt.leaves.nodes() { - require.NotEqual(t, leaf.hash, finalised.hash) - require.True(t, leaf.isDescendantOf(finalised)) + // parentNode node here will be the latest block in the tree + // that means it will be the leaf as well + blockTree.leaves.store(parentNode.hash, parentNode) + +} + +func Test_BlockTree_GetBlockRuntime(t *testing.T) { + // {0x00} -> {0x01} -> {0x02} -> {0x03} + // -> {0x04} -> {0x05} + // -> {0x06} + blockTree := buildLinearBlockTree(t, 4) + + appendForksAt(t, blockTree, common.MustHexToHash("0x01"), + common.MustHexToHash("0x04"), + common.MustHexToHash("0x05")) + + appendForksAt(t, blockTree, common.MustHexToHash("0x04"), + common.MustHexToHash("0x06")) + + rootRuntime := NewMockInstance(nil) + lastCanonicalRuntime := NewMockInstance(nil) + forkedRuntime := NewMockInstance(nil) + + appendRuntimeToHash(t, blockTree, common.MustHexToHash("0x00"), rootRuntime) + appendRuntimeToHash(t, blockTree, common.MustHexToHash("0x03"), lastCanonicalRuntime) + appendRuntimeToHash(t, blockTree, common.MustHexToHash("0x04"), forkedRuntime) + appendRuntimeToHash(t, blockTree, common.MustHexToHash("0x05"), lastCanonicalRuntime) + + // Even though we have only 3 runtimes (rootRuntime, lastCanonicalRuntime and forkedRuntime) + // the lastCanonicalRuntime happens in different forks, it is in the block `0x03` and in block + // `0x05` and both blocks don't have any relashionship that justifies the usage of one instance + const totalRuntimesInMemory = 4 + require.Equal(t, totalRuntimesInMemory, len(blockTree.runtimes.mapping)) + + testCases := []struct { + hashInput common.Hash + expectedRuntime runtime.Instance + }{ + {common.MustHexToHash("0x06"), forkedRuntime}, + {common.MustHexToHash("0x05"), lastCanonicalRuntime}, + {common.MustHexToHash("0x04"), forkedRuntime}, + {common.MustHexToHash("0x03"), lastCanonicalRuntime}, + {common.MustHexToHash("0x02"), rootRuntime}, + {common.MustHexToHash("0x00"), rootRuntime}, + } + + for _, tt := range testCases { + givenRuntime, err := blockTree.GetBlockRuntime(tt.hashInput) + require.NoError(t, err) + if tt.expectedRuntime != givenRuntime { + t.Errorf("exepected %v. got %v", tt.expectedRuntime, givenRuntime) + return + } } } +func Test_BlockTree_Prune(t *testing.T) { + t.Parallel() + + t.Run("finalised_hash_is_root_hash", func(t *testing.T) { + t.Parallel() + + blockTree := &BlockTree{ + root: &node{ + hash: Hash{1}, + }, + } + + pruned := blockTree.Prune(Hash{1}) + + assert.Empty(t, pruned) + }) + + t.Run("node_not_found", func(t *testing.T) { + t.Parallel() + + blockTree := &BlockTree{ + root: &node{}, + leaves: newEmptyLeafMap(), + } + + pruned := blockTree.Prune(Hash{1}) + + assert.Empty(t, pruned) + }) + + t.Run("nothing_to_prune", func(t *testing.T) { + t.Parallel() + + rootNode := &node{ + hash: common.Hash{1}, + number: 0, + } + blockTree := &BlockTree{ + root: rootNode, + leaves: newEmptyLeafMap(), + runtimes: newHashToRuntime(), + } + + ctrl := gomock.NewController(t) + runtimeInstanceToBePrunned := NewMockInstance(ctrl) + runtimeInstanceToBePrunned.EXPECT().Stop() + blockTree.runtimes.set(common.Hash{1}, runtimeInstanceToBePrunned) + + // {1} -> {2} + parent := rootNode + newNode := &node{ + parent: parent, + hash: common.Hash{2}, + number: 1, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + expectedRuntimeInstance := NewMockInstance(nil) + blockTree.runtimes.set(common.Hash{2}, expectedRuntimeInstance) + + pruned := blockTree.Prune(common.Hash{2}) + assert.Empty(t, pruned) + + expectedHashToRuntime := &hashToRuntime{ + mapping: map[common.Hash]runtime.Instance{ + {2}: expectedRuntimeInstance, + }, + } + assert.Equal(t, expectedHashToRuntime, blockTree.runtimes) + }) + + t.Run("prune_canonical_runtimes", func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + rootNode := &node{ + hash: common.Hash{1}, + number: 0, + } + rootRuntime := NewMockInstance(ctrl) + blockTree := &BlockTree{ + root: rootNode, + leaves: newEmptyLeafMap(), + runtimes: newHashToRuntime(), + } + blockTree.runtimes.set(common.Hash{1}, rootRuntime) + + // {1} -> {2} + parent := rootNode + newNode := &node{ + parent: parent, + hash: common.Hash{2}, + number: 1, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + leafRuntime := NewMockInstance(ctrl) + blockTree.runtimes.set(common.Hash{2}, leafRuntime) + + // Previous runtime is pruned + rootRuntime.EXPECT().Stop() + + pruned := blockTree.Prune(common.Hash{2}) + assert.Empty(t, pruned) + + expectedHashToRuntime := &hashToRuntime{ + mapping: map[common.Hash]runtime.Instance{ + {2}: leafRuntime, + }, + } + assert.Equal(t, expectedHashToRuntime, blockTree.runtimes) + }) + + t.Run("prune_fork", func(t *testing.T) { + t.Parallel() + + rootNode := &node{ + hash: common.Hash{1}, + number: 0, + } + + blockTree := &BlockTree{ + root: rootNode, + leaves: newEmptyLeafMap(), + runtimes: newHashToRuntime(), + } + + rootRuntime := NewMockInstance(nil) + blockTree.runtimes.set(common.Hash{1}, rootRuntime) + + // {1} -> {2} + // we don't need to add a runtime to node number 2 since + parent := rootNode + newNode := &node{ + parent: parent, + hash: common.Hash{2}, + number: 1, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + // {1} -> {2} + // -> {3} + parent = rootNode + newNode = &node{ + parent: parent, + hash: common.Hash{3}, + number: 1, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + ctrl := gomock.NewController(t) + runtimeToBePrunned := NewMockInstance(ctrl) + runtimeToBePrunned.EXPECT().Stop() + + blockTree.runtimes.set(common.Hash{3}, runtimeToBePrunned) + + // expect that node number 3 to be prunned with its runtime + pruned := blockTree.Prune(common.Hash{2}) + assert.Equal(t, []common.Hash{{3}}, pruned) + expectedHashToRuntime := &hashToRuntime{ + mapping: map[common.Hash]runtime.Instance{ + {2}: rootRuntime, + }, + } + assert.Equal(t, expectedHashToRuntime, blockTree.runtimes) + }) + + t.Run("complex_example", func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + rootNode := &node{ + hash: common.Hash{1}, + number: 100, + } + rootRuntime := NewMockInstance(ctrl) + rootRuntime.EXPECT().Stop() + + blockTree := &BlockTree{ + root: rootNode, + leaves: newEmptyLeafMap(), + runtimes: &hashToRuntime{ + mapping: map[common.Hash]runtime.Instance{ + {1}: rootRuntime, + }, + }, + } + + // {1} -> rootRuntime + // {1} -> {2} + parent := rootNode + newNode := &node{ + parent: parent, + hash: common.Hash{2}, + number: 101, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + // {1} -> rootRuntime | {3} -> lastCanonicalRuntime + // {1} -> {2} -> {3} + parent = newNode + newNode = &node{ + parent: parent, + hash: common.Hash{3}, + number: 102, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + lastCanonicalRuntime := NewMockInstance(ctrl) + blockTree.runtimes.set(common.Hash{3}, lastCanonicalRuntime) + + // {1} -> rootRuntime | {3} -> lastCanonicalRuntime + // {1} -> {2} -> {3} -> {4} + parent = newNode + newNode = &node{ + parent: parent, + hash: common.Hash{4}, + number: 103, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + // {1} -> rootRuntime | {3} -> lastCanonicalRuntime + // {1} -> {2} -> {3} -> {4} + // -> {5} + parent = blockTree.getNode(common.Hash{2}) + newNode = &node{ + parent: parent, + hash: common.Hash{5}, + number: 102, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + + // {1} -> rootRuntime | {3} -> lastCanonicalRuntime + // {1} -> rootRuntime | {6} -> lastCanonicalRuntime + // {1} -> {2} -> {3} -> {4} + // -> {5} -> {6} + parent = blockTree.getNode(common.Hash{5}) + newNode = &node{ + parent: parent, + hash: common.Hash{6}, + number: 103, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + blockTree.runtimes.set(common.Hash{6}, lastCanonicalRuntime) + + // {1} -> rootRuntime | {3} -> lastCanonicalRuntime + // {1} -> rootRuntime | {6} -> lastCanonicalRuntime | {7} -> forkedRuntime + // {1} -> {2} -> {3} -> {4} + // -> {5} -> {6} + // -> {7} + parent = blockTree.getNode(common.Hash{5}) + newNode = &node{ + parent: parent, + hash: common.Hash{7}, + number: 102, + } + parent.addChild(newNode) + blockTree.leaves.replace(parent, newNode) + forkedRuntime := NewMockInstance(ctrl) + forkedRuntime.EXPECT().Stop() + blockTree.runtimes.set(common.Hash{7}, forkedRuntime) + + pruned := blockTree.Prune(common.Hash{4}) + assert.Equal(t, []common.Hash{{5}, {6}, {7}}, pruned) + + expectedHashToRuntime := &hashToRuntime{ + mapping: map[common.Hash]runtime.Instance{ + {4}: lastCanonicalRuntime, + }, + } + assert.Equal(t, expectedHashToRuntime, blockTree.runtimes) + }) +} + func Test_BlockTree_GetHashByNumber(t *testing.T) { bt, _ := createTestBlockTree(t, testHeader, 8) best := bt.BestBlockHash() diff --git a/lib/blocktree/hashtoruntime.go b/lib/blocktree/hashtoruntime.go index 3042e4afea..7a132c060f 100644 --- a/lib/blocktree/hashtoruntime.go +++ b/lib/blocktree/hashtoruntime.go @@ -6,7 +6,9 @@ package blocktree import ( "sync" + "github.com/ChainSafe/gossamer/lib/common" "github.com/ChainSafe/gossamer/lib/runtime" + "golang.org/x/exp/maps" ) type hashToRuntime struct { @@ -30,10 +32,77 @@ func (h *hashToRuntime) set(hash Hash, instance runtime.Instance) { h.mutex.Lock() defer h.mutex.Unlock() h.mapping[hash] = instance + inMemoryRuntimesGauge.Inc() } func (h *hashToRuntime) delete(hash Hash) { h.mutex.Lock() defer h.mutex.Unlock() delete(h.mapping, hash) + inMemoryRuntimesGauge.Dec() +} + +func (h *hashToRuntime) hashes() (hashes []common.Hash) { + h.mutex.RLock() + defer h.mutex.RUnlock() + + return maps.Keys(h.mapping) +} + +// onFinalisation handles pruning and recording on block finalisation. +// newCanonicalBlockHashes is the block hashes of the blocks newly finalised. +// The last element is the finalised block hash. +func (h *hashToRuntime) onFinalisation(newCanonicalBlockHashes []common.Hash) { + h.mutex.Lock() + defer h.mutex.Unlock() + + if len(h.mapping) == 0 { + logger.Warnf("no runtimes in the mapping") + return + } + + defer func() { + totalInMemoryRuntimes := len(h.mapping) + inMemoryRuntimesGauge.Set(float64(totalInMemoryRuntimes)) + }() + + finalisedHash := newCanonicalBlockHashes[len(newCanonicalBlockHashes)-1] + // if there is only one runtime in the mapping then we should update + // its key so we don't need to lookup the entire chain in order to find the ancestry + if len(h.mapping) == 1 { + uniqueAvailableInstance := maps.Values(h.mapping)[0] + + h.mapping = make(map[Hash]runtime.Instance) + h.mapping[finalisedHash] = uniqueAvailableInstance + return + } + + // we procced from backwards since the last element in the newCanonicalBlockHashes + // is the finalized one, verifying if there is a runtime instance closest to the finalized + // hash. When we find it we clear all the map entries and keeping only the instance found + // with the finalised hash as the key + lastElementIdx := len(newCanonicalBlockHashes) - 1 + for idx := lastElementIdx; idx >= 0; idx-- { + currentHash := newCanonicalBlockHashes[idx] + inMemoryRuntime := h.mapping[currentHash] + + if inMemoryRuntime != nil { + // stop all the running instances created by forks keeping + // just the closest instance to the finalized block hash + stoppedRuntimes := make(map[runtime.Instance]struct{}) + for _, runtimeToPrune := range h.mapping { + if inMemoryRuntime != runtimeToPrune { + _, stopped := stoppedRuntimes[runtimeToPrune] + if !stopped { + runtimeToPrune.Stop() + stoppedRuntimes[runtimeToPrune] = struct{}{} + } + } + } + + h.mapping = make(map[Hash]runtime.Instance) + h.mapping[finalisedHash] = inMemoryRuntime + return + } + } } diff --git a/lib/blocktree/hashtoruntime_test.go b/lib/blocktree/hashtoruntime_test.go index 0996488be4..99eff79153 100644 --- a/lib/blocktree/hashtoruntime_test.go +++ b/lib/blocktree/hashtoruntime_test.go @@ -11,6 +11,7 @@ import ( "github.com/ChainSafe/gossamer/lib/common" runtime "github.com/ChainSafe/gossamer/lib/runtime" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" ) @@ -64,6 +65,27 @@ func Test_hashToRuntime_get(t *testing.T) { } } +func Test_hashToRuntime_hashes(t *testing.T) { + t.Parallel() + + htr := &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {4, 5, 6}: NewMockInstance(nil), + {7, 8, 9}: NewMockInstance(nil), + {1, 2, 3}: NewMockInstance(nil), + }, + } + + expectedHashes := []common.Hash{ + {7, 8, 9}, + {4, 5, 6}, + {1, 2, 3}, + } + + hashes := htr.hashes() + assert.ElementsMatch(t, expectedHashes, hashes) +} + func Test_hashToRuntime_set(t *testing.T) { t.Parallel() @@ -159,6 +181,123 @@ func Test_hashToRuntime_delete(t *testing.T) { } } +func Test_hashToRuntime_onFinalisation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + makeParameters func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) + newCanonicalBlockHashes []Hash + }{ + "new_finalised_runtime_not_found": { + makeParameters: func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) { + return &hashToRuntime{}, &hashToRuntime{} + }, + newCanonicalBlockHashes: []Hash{{1}}, + }, + "prune_fork_runtime_with_a_unique_instance": { + makeParameters: func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) { + finalisedRuntime := NewMockInstance(ctrl) + initial = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {1}: finalisedRuntime, + }, + } + + // keep the instance but update the key + expected = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {2}: finalisedRuntime, + }, + } + return initial, expected + }, + newCanonicalBlockHashes: []Hash{{2}}, + }, + "prune_fork_runtimes_only": { + makeParameters: func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) { + finalisedRuntime := NewMockInstance(ctrl) + prunedForkRuntime := NewMockInstance(ctrl) + prunedForkRuntime.EXPECT().Stop() + initial = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {1}: finalisedRuntime, + {3}: prunedForkRuntime, + }, + } + expected = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {1}: finalisedRuntime, + }, + } + return initial, expected + }, + newCanonicalBlockHashes: []Hash{{1}}, + }, + "new_canonical_block_hash_not_found": { + makeParameters: func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) { + newFinalisedRuntime := NewMockInstance(ctrl) + initial = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + // missing {1} + {2}: newFinalisedRuntime, + }, + } + expected = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {2}: newFinalisedRuntime, + }, + } + return initial, expected + }, + newCanonicalBlockHashes: []Hash{{1}, {2}}, + }, + "prune_fork_and_canonical_runtimes": { + makeParameters: func(ctrl *gomock.Controller) (initial, expected *hashToRuntime) { + finalisedRuntime := NewMockInstance(ctrl) + unfinalisedRuntime := NewMockInstance(ctrl) + newFinalisedRuntime := NewMockInstance(ctrl) + prunedForkRuntime := NewMockInstance(ctrl) + + finalisedRuntime.EXPECT().Stop() + unfinalisedRuntime.EXPECT().Stop() + prunedForkRuntime.EXPECT().Stop() + + initial = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + // Previously finalised chain + {0}: finalisedRuntime, + // Newly finalised chain + {3}: unfinalisedRuntime, + {5}: newFinalisedRuntime, + // Runtimes from forks + {100}: prunedForkRuntime, + }, + } + expected = &hashToRuntime{ + mapping: map[Hash]runtime.Instance{ + {6}: newFinalisedRuntime, + }, + } + return initial, expected + }, + newCanonicalBlockHashes: []Hash{{2}, {3}, {4}, {5}, {6}}, + }, + } + + for name, testCase := range testCases { + testCase := testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + htr, expectedHtr := testCase.makeParameters(ctrl) + htr.onFinalisation(testCase.newCanonicalBlockHashes) + + assert.Equal(t, expectedHtr, htr) + }) + } +} + func Test_hashToRuntime_threadSafety(t *testing.T) { // This test consists in checking for concurrent access // using the -race detector.