Skip to content

Commit

Permalink
refactor(stf): remove RunWithCtx (#21739)
Browse files Browse the repository at this point in the history
(cherry picked from commit d6364b8)

# Conflicts:
#	core/header/service.go
#	core/store/service.go
#	runtime/v2/builder.go
#	runtime/v2/go.mod
#	runtime/v2/go.sum
#	runtime/v2/module.go
#	server/v2/appmanager/appmanager.go
#	server/v2/appmanager/genesis.go
#	server/v2/appmanager/types.go
#	server/v2/stf/stf.go
#	simapp/v2/go.mod
#	simapp/v2/go.sum
  • Loading branch information
kocubinski authored and mergify[bot] committed Sep 19, 2024
1 parent a1e7ef0 commit 532b554
Show file tree
Hide file tree
Showing 16 changed files with 2,051 additions and 2 deletions.
92 changes: 92 additions & 0 deletions core/header/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package header

import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"time"
)

// Service defines the interface in which you can get header information
type Service interface {
HeaderInfo(context.Context) Info
}

// Info defines a struct that contains information about the header
type Info struct {
Height int64 // Height returns the height of the block
Hash []byte // Hash returns the hash of the block header
Time time.Time // Time returns the time of the block
AppHash []byte // AppHash used in the current block header
ChainID string // ChainId returns the chain ID of the block
}

const hashSize = sha256.Size

// Bytes encodes the Info struct into a byte slice using little-endian encoding
func (i *Info) Bytes() ([]byte, error) {
buf := make([]byte, 0)

// Encode Height
heightBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(heightBytes, uint64(i.Height))
buf = append(buf, heightBytes...)

// Encode Hash
if len(i.Hash) != hashSize {
return nil, errors.New("invalid Hash size")
}

buf = append(buf, i.Hash...)

// Encode Time
timeBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(timeBytes, uint64(i.Time.Unix()))
buf = append(buf, timeBytes...)

// Encode AppHash
if len(i.AppHash) != hashSize {
return nil, errors.New("invalid AppHash size")
}
buf = append(buf, i.AppHash...)

// Encode ChainID
chainIDLen := len(i.ChainID)
buf = append(buf, byte(chainIDLen))
buf = append(buf, []byte(i.ChainID)...)

return buf, nil
}

// FromBytes decodes the byte slice into an Info struct using little-endian encoding
func (i *Info) FromBytes(bytes []byte) error {
// Decode Height
i.Height = int64(binary.LittleEndian.Uint64(bytes[:8]))
bytes = bytes[8:]

// Decode Hash
i.Hash = make([]byte, hashSize)
copy(i.Hash, bytes[:hashSize])
bytes = bytes[hashSize:]

// Decode Time
unixTime := int64(binary.LittleEndian.Uint64(bytes[:8]))
i.Time = time.Unix(unixTime, 0).UTC()
bytes = bytes[8:]

// Decode AppHash
i.AppHash = make([]byte, hashSize)
copy(i.AppHash, bytes[:hashSize])
bytes = bytes[hashSize:]

// Decode ChainID
chainIDLen := int(bytes[0])
bytes = bytes[1:]
if len(bytes) < chainIDLen {
return errors.New("invalid byte slice length")
}
i.ChainID = string(bytes[:chainIDLen])

return nil
}
34 changes: 34 additions & 0 deletions core/store/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package store

import "context"

// KVStoreService represents a unique, non-forgeable handle to a regular merkle-tree
// backed KVStore. It should be provided as a module-scoped dependency by the runtime
// module being used to build the app.
type KVStoreService interface {
// OpenKVStore retrieves the KVStore from the context.
OpenKVStore(context.Context) KVStore

Check failure on line 10 in core/store/service.go

View workflow job for this annotation

GitHub Actions / dependency-review

undefined: KVStore
}

// KVStoreServiceFactory is a function that creates a new KVStoreService.
// It can be used to override the default KVStoreService bindings for cases
// where an application must supply a custom stateful backend.
type KVStoreServiceFactory func([]byte) KVStoreService

// MemoryStoreService represents a unique, non-forgeable handle to a memory-backed
// KVStore. It should be provided as a module-scoped dependency by the runtime
// module being used to build the app.
type MemoryStoreService interface {
// OpenMemoryStore retrieves the memory store from the context.
OpenMemoryStore(context.Context) KVStore

Check failure on line 23 in core/store/service.go

View workflow job for this annotation

GitHub Actions / dependency-review

undefined: KVStore
}

// TransientStoreService represents a unique, non-forgeable handle to a memory-backed
// KVStore which is reset at the start of every block. It should be provided as
// a module-scoped dependency by the runtime module being used to build the app.
// WARNING: This service is not available in server/v2 apps. Store/v2 does not support
// transient stores.
type TransientStoreService interface {
// OpenTransientStore retrieves the transient store from the context.
OpenTransientStore(context.Context) KVStore

Check failure on line 33 in core/store/service.go

View workflow job for this annotation

GitHub Actions / dependency-review

undefined: KVStore
}
259 changes: 259 additions & 0 deletions runtime/v2/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package runtime

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"path/filepath"

"cosmossdk.io/core/appmodule"
appmodulev2 "cosmossdk.io/core/appmodule/v2"
"cosmossdk.io/core/server"
"cosmossdk.io/core/store"
"cosmossdk.io/core/transaction"
"cosmossdk.io/runtime/v2/services"
"cosmossdk.io/server/v2/appmanager"
"cosmossdk.io/server/v2/stf"
"cosmossdk.io/server/v2/stf/branch"
"cosmossdk.io/store/v2/db"
rootstore "cosmossdk.io/store/v2/root"
)

// AppBuilder is a type that is injected into a container by the runtime/v2 module
// (as *AppBuilder) which can be used to create an app which is compatible with
// the existing app.go initialization conventions.
type AppBuilder[T transaction.Tx] struct {
app *App[T]
config server.DynamicConfig
storeOptions *rootstore.Options

// the following fields are used to overwrite the default
branch func(state store.ReaderMap) store.WriterMap
txValidator func(ctx context.Context, tx T) error
postTxExec func(ctx context.Context, tx T, success bool) error
}

// DefaultGenesis returns a default genesis from the registered AppModule's.
func (a *AppBuilder[T]) DefaultGenesis() map[string]json.RawMessage {
return a.app.moduleManager.DefaultGenesis()
}

// RegisterModules registers the provided modules with the module manager.
// This is the primary hook for integrating with modules which are not registered using the app config.
func (a *AppBuilder[T]) RegisterModules(modules map[string]appmodulev2.AppModule) error {
for name, appModule := range modules {
// if a (legacy) module implements the HasName interface, check that the name matches
if mod, ok := appModule.(interface{ Name() string }); ok {
if name != mod.Name() {
a.app.logger.Warn(fmt.Sprintf("module name %q does not match name returned by HasName: %q", name, mod.Name()))
}
}

if _, ok := a.app.moduleManager.modules[name]; ok {
return fmt.Errorf("module named %q already exists", name)
}
a.app.moduleManager.modules[name] = appModule

if mod, ok := appModule.(appmodulev2.HasRegisterInterfaces); ok {
mod.RegisterInterfaces(a.app.interfaceRegistrar)
}

if mod, ok := appModule.(appmodule.HasAminoCodec); ok {
mod.RegisterLegacyAminoCodec(a.app.amino)
}
}

return nil
}

// RegisterStores registers the provided store keys.
// This method should only be used for registering extra stores
// which is necessary for modules that not registered using the app config.
// To be used in combination of RegisterModules.
func (a *AppBuilder[T]) RegisterStores(keys ...string) {
a.app.storeKeys = append(a.app.storeKeys, keys...)
}

// Build builds an *App instance.
func (a *AppBuilder[T]) Build(opts ...AppBuilderOption[T]) (*App[T], error) {
for _, opt := range opts {
opt(a)
}

// default branch
if a.branch == nil {
a.branch = branch.DefaultNewWriterMap
}

// default tx validator
if a.txValidator == nil {
a.txValidator = a.app.moduleManager.TxValidators()
}

// default post tx exec
if a.postTxExec == nil {
a.postTxExec = func(ctx context.Context, tx T, success bool) error {
return nil
}
}

if err := a.app.moduleManager.RegisterServices(a.app); err != nil {
return nil, err
}

endBlocker, valUpdate := a.app.moduleManager.EndBlock()

stf, err := stf.NewSTF[T](
a.app.logger.With("module", "stf"),
a.app.msgRouterBuilder,
a.app.queryRouterBuilder,
a.app.moduleManager.PreBlocker(),
a.app.moduleManager.BeginBlock(),
endBlocker,
a.txValidator,
valUpdate,
a.postTxExec,
a.branch,
)
if err != nil {
return nil, fmt.Errorf("failed to create STF: %w", err)
}
a.app.stf = stf

home := a.config.GetString(FlagHome)
scRawDb, err := db.NewDB(
db.DBType(a.config.GetString("store.app-db-backend")),
"application",
filepath.Join(home, "data"),
nil,
)
if err != nil {
panic(err)
}

var storeOptions rootstore.Options
if a.storeOptions != nil {
storeOptions = *a.storeOptions
} else {
storeOptions = rootstore.DefaultStoreOptions()
}
factoryOptions := &rootstore.FactoryOptions{
Logger: a.app.logger,
RootDir: home,
Options: storeOptions,
StoreKeys: append(a.app.storeKeys, "stf"),
SCRawDB: scRawDb,
}

rs, err := rootstore.CreateRootStore(factoryOptions)
if err != nil {
return nil, fmt.Errorf("failed to create root store: %w", err)
}
a.app.db = rs

appManagerBuilder := appmanager.Builder[T]{
STF: a.app.stf,
DB: a.app.db,
ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit,
QueryGasLimit: a.app.config.GasConfig.QueryGasLimit,
SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit,
InitGenesis: func(
ctx context.Context,
src io.Reader,
txHandler func(json.RawMessage) error,
) (store.WriterMap, error) {
// this implementation assumes that the state is a JSON object
bz, err := io.ReadAll(src)
if err != nil {
return nil, fmt.Errorf("failed to read import state: %w", err)
}
var genesisJSON map[string]json.RawMessage
if err = json.Unmarshal(bz, &genesisJSON); err != nil {
return nil, err
}

v, zeroState, err := a.app.db.StateLatest()
if err != nil {
return nil, fmt.Errorf("unable to get latest state: %w", err)
}
if v != 0 { // TODO: genesis state may be > 0, we need to set version on store
return nil, errors.New("cannot init genesis on non-zero state")
}
genesisCtx := services.NewGenesisContext(a.branch(zeroState))
genesisState, err := genesisCtx.Run(ctx, func(ctx context.Context) error {
err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler)
if err != nil {
return fmt.Errorf("failed to init genesis: %w", err)
}
return nil
})

return genesisState, err
},
ExportGenesis: func(ctx context.Context, version uint64) ([]byte, error) {
_, state, err := a.app.db.StateLatest()
if err != nil {
return nil, fmt.Errorf("unable to get latest state: %w", err)
}
genesisCtx := services.NewGenesisContext(a.branch(state))

var genesisJson map[string]json.RawMessage
_, err = genesisCtx.Run(ctx, func(ctx context.Context) error {
genesisJson, err = a.app.moduleManager.ExportGenesisForModules(ctx)
return err
})
if err != nil {
return nil, fmt.Errorf("failed to export genesis: %w", err)
}

bz, err := json.Marshal(genesisJson)
if err != nil {
return nil, fmt.Errorf("failed to marshal genesis: %w", err)
}

return bz, nil
},
}

appManager, err := appManagerBuilder.Build()
if err != nil {
return nil, fmt.Errorf("failed to build app manager: %w", err)
}
a.app.AppManager = appManager

return a.app, nil
}

// AppBuilderOption is a function that can be passed to AppBuilder.Build to customize the resulting app.
type AppBuilderOption[T transaction.Tx] func(*AppBuilder[T])

// AppBuilderWithBranch sets a custom branch implementation for the app.
func AppBuilderWithBranch[T transaction.Tx](branch func(state store.ReaderMap) store.WriterMap) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.branch = branch
}
}

// AppBuilderWithTxValidator sets the tx validator for the app.
// It overrides all default tx validators defined by modules.
func AppBuilderWithTxValidator[T transaction.Tx](txValidators func(ctx context.Context, tx T) error) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.txValidator = txValidators
}
}

// AppBuilderWithPostTxExec sets logic that will be executed after each transaction.
// When not provided, a no-op function will be used.
func AppBuilderWithPostTxExec[T transaction.Tx](postTxExec func(ctx context.Context, tx T, success bool) error) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.postTxExec = postTxExec
}
}

func AppBuilderWithStoreOptions[T transaction.Tx](opts *rootstore.Options) AppBuilderOption[T] {
return func(a *AppBuilder[T]) {
a.storeOptions = opts
}
}
Loading

0 comments on commit 532b554

Please sign in to comment.