-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
…r vote extensions (#22830) (cherry picked from commit 6d12b1d) # Conflicts: # runtime/v2/builder.go
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
package runtime | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
|
||
"cosmossdk.io/core/appmodule" | ||
appmodulev2 "cosmossdk.io/core/appmodule/v2" | ||
"cosmossdk.io/core/store" | ||
"cosmossdk.io/core/transaction" | ||
"cosmossdk.io/runtime/v2/services" | ||
Check failure on line 14 in runtime/v2/builder.go GitHub Actions / dependency-review
Check failure on line 14 in runtime/v2/builder.go GitHub Actions / dependency-review
|
||
"cosmossdk.io/server/v2/appmanager" | ||
Check failure on line 15 in runtime/v2/builder.go GitHub Actions / dependency-review
Check failure on line 15 in runtime/v2/builder.go GitHub Actions / dependency-review
|
||
"cosmossdk.io/server/v2/stf" | ||
Check failure on line 16 in runtime/v2/builder.go GitHub Actions / dependency-review
Check failure on line 16 in runtime/v2/builder.go GitHub Actions / dependency-review
|
||
"cosmossdk.io/server/v2/stf/branch" | ||
Check failure on line 17 in runtime/v2/builder.go GitHub Actions / dependency-review
Check failure on line 17 in runtime/v2/builder.go GitHub Actions / dependency-review
|
||
"cosmossdk.io/store/v2/root" | ||
Check failure on line 18 in runtime/v2/builder.go GitHub Actions / dependency-review
Check failure on line 18 in runtime/v2/builder.go GitHub Actions / dependency-review
|
||
) | ||
|
||
// 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] | ||
storeBuilder root.Builder | ||
storeConfig *root.Config | ||
|
||
// 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 | ||
preblocker func(ctx context.Context, txs []T, mmPreblocker func() error) error | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
// 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 | ||
} | ||
} | ||
|
||
var err error | ||
a.app.db, err = a.storeBuilder.Build(a.app.logger, a.storeConfig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err = a.app.moduleManager.RegisterServices(a.app); err != nil { | ||
return nil, err | ||
} | ||
|
||
endBlocker, valUpdate := a.app.moduleManager.EndBlock() | ||
|
||
preblockerFn := func(ctx context.Context, txs []T) error { | ||
if a.preblocker != nil { | ||
return a.preblocker(ctx, txs, func() error { | ||
return a.app.moduleManager.PreBlocker()(ctx, txs) | ||
}) | ||
} | ||
|
||
// if there is no preblocker set, call the module manager's preblocker directly | ||
return a.app.moduleManager.PreBlocker()(ctx, txs) | ||
} | ||
|
||
stf, err := stf.New[T]( | ||
a.app.logger.With("module", "stf"), | ||
a.app.msgRouterBuilder, | ||
a.app.queryRouterBuilder, | ||
preblockerFn, | ||
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 | ||
|
||
a.app.AppManager = appmanager.New[T]( | ||
appmanager.Config{ | ||
ValidateTxGasLimit: a.app.config.GasConfig.ValidateTxGasLimit, | ||
QueryGasLimit: a.app.config.GasConfig.QueryGasLimit, | ||
SimulationGasLimit: a.app.config.GasConfig.SimulationGasLimit, | ||
}, | ||
a.app.db, | ||
a.app.stf, | ||
a.initGenesis, | ||
a.exportGenesis, | ||
) | ||
|
||
return a.app, nil | ||
} | ||
|
||
// initGenesis returns the app initialization genesis for modules | ||
func (a *AppBuilder[T]) initGenesis(ctx context.Context, src io.Reader, txHandler func(json.RawMessage) error) (store.WriterMap, []appmodule.ValidatorUpdate, error) { | ||
// this implementation assumes that the state is a JSON object | ||
bz, err := io.ReadAll(src) | ||
if err != nil { | ||
return nil, 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, nil, err | ||
} | ||
|
||
v, zeroState, err := a.app.db.StateLatest() | ||
if err != nil { | ||
return nil, 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, nil, errors.New("cannot init genesis on non-zero state") | ||
} | ||
genesisCtx := services.NewGenesisContext(a.branch(zeroState)) | ||
var valUpdates []appmodulev2.ValidatorUpdate | ||
genesisState, err := genesisCtx.Mutate(ctx, func(ctx context.Context) error { | ||
valUpdates, err = a.app.moduleManager.InitGenesisJSON(ctx, genesisJSON, txHandler) | ||
if err != nil { | ||
return fmt.Errorf("failed to init genesis: %w", err) | ||
} | ||
|
||
return nil | ||
}) | ||
|
||
return genesisState, valUpdates, err | ||
} | ||
|
||
// exportGenesis returns the app export genesis logic for modules | ||
func (a *AppBuilder[T]) exportGenesis(ctx context.Context, version uint64) ([]byte, error) { | ||
state, err := a.app.db.StateAt(version) | ||
if err != nil { | ||
return nil, fmt.Errorf("unable to get state at given version: %w", err) | ||
} | ||
|
||
genesisJson, err := a.app.moduleManager.ExportGenesisForModules( | ||
ctx, | ||
func() store.WriterMap { | ||
return a.branch(state) | ||
}, | ||
) | ||
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 | ||
} | ||
|
||
// 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 | ||
} | ||
} | ||
|
||
// AppBuilderWithPreblocker sets logic that will be executed before each block. | ||
// mmPreblocker can be used to call module manager's preblocker, so that it can be | ||
// called before or after depending on the app's logic. | ||
// This is especially useful when implementing vote extensions. | ||
func AppBuilderWithPreblocker[T transaction.Tx]( | ||
preblocker func( | ||
ctx context.Context, txs []T, mmPreblocker func() error, | ||
) error, | ||
) AppBuilderOption[T] { | ||
return func(a *AppBuilder[T]) { | ||
a.preblocker = preblocker | ||
} | ||
} |