cosmos-sdk/server/v2/appmanager/appmanager.go

174 lines
5.7 KiB
Go

package appmanager
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
appmanager "cosmossdk.io/core/app"
corestore "cosmossdk.io/core/store"
"cosmossdk.io/core/transaction"
)
// Store defines the underlying storage behavior needed by AppManager.
type Store interface {
// StateLatest returns a readonly view over the latest
// committed state of the store. Alongside the version
// associated with it.
StateLatest() (uint64, corestore.ReaderMap, error)
// StateAt returns a readonly view over the provided
// state. Must error when the version does not exist.
StateAt(version uint64) (corestore.ReaderMap, error)
}
// AppManager is a coordinator for all things related to an application
type AppManager[T transaction.Tx] struct {
config Config
db Store
initGenesis InitGenesis
exportGenesis ExportGenesis
stf StateTransitionFunction[T]
}
func (a AppManager[T]) InitGenesis(
ctx context.Context,
blockRequest *appmanager.BlockRequest[T],
initGenesisJSON []byte,
txDecoder transaction.Codec[T],
) (*appmanager.BlockResponse, corestore.WriterMap, error) {
v, zeroState, err := a.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")
}
var genTxs []T
genesisState, err := a.stf.RunWithCtx(ctx, zeroState, func(ctx context.Context) error {
return a.initGenesis(ctx, bytes.NewBuffer(initGenesisJSON), func(jsonTx json.RawMessage) error {
genTx, err := txDecoder.DecodeJSON(jsonTx)
if err != nil {
return fmt.Errorf("failed to decode genesis transaction: %w", err)
}
genTxs = append(genTxs, genTx)
return nil
})
})
if err != nil {
return nil, nil, fmt.Errorf("failed to import genesis state: %w", err)
}
// run block
// TODO: in an ideal world, genesis state is simply an initial state being applied
// unaware of what that state means in relation to every other
blockRequest.Txs = genTxs
blockResponse, blockZeroState, err := a.stf.DeliverBlock(ctx, blockRequest, genesisState)
if err != nil {
return blockResponse, nil, fmt.Errorf("failed to deliver block %d: %w", blockRequest.Height, err)
}
// after executing block 0, we extract the changes and apply them to the genesis state.
blockZeroStateChanges, err := blockZeroState.GetStateChanges()
if err != nil {
return nil, nil, fmt.Errorf("failed to get block zero state changes: %w", err)
}
err = genesisState.ApplyStateChanges(blockZeroStateChanges)
if err != nil {
return nil, nil, fmt.Errorf("failed to apply block zero state changes to genesis state: %w", err)
}
return blockResponse, genesisState, err
// consensus server will need to set the version of the store
}
// ExportGenesis exports the genesis state of the application.
func (a AppManager[T]) ExportGenesis(ctx context.Context, version uint64) ([]byte, error) {
bz, err := a.exportGenesis(ctx, version)
if err != nil {
return nil, fmt.Errorf("failed to export genesis state: %w", err)
}
return bz, nil
}
func (a AppManager[T]) DeliverBlock(
ctx context.Context,
block *appmanager.BlockRequest[T],
) (*appmanager.BlockResponse, corestore.WriterMap, error) {
latestVersion, currentState, err := a.db.StateLatest()
if err != nil {
return nil, nil, fmt.Errorf("unable to create new state for height %d: %w", block.Height, err)
}
if latestVersion+1 != block.Height {
return nil, nil, fmt.Errorf("invalid DeliverBlock height wanted %d, got %d", latestVersion+1, block.Height)
}
blockResponse, newState, err := a.stf.DeliverBlock(ctx, block, currentState)
if err != nil {
return nil, nil, fmt.Errorf("block delivery failed: %w", err)
}
return blockResponse, newState, nil
}
// ValidateTx will validate the tx against the latest storage state. This means that
// only the stateful validation will be run, not the execution portion of the tx.
// If full execution is needed, Simulate must be used.
func (a AppManager[T]) ValidateTx(ctx context.Context, tx T) (appmanager.TxResult, error) {
_, latestState, err := a.db.StateLatest()
if err != nil {
return appmanager.TxResult{}, err
}
return a.stf.ValidateTx(ctx, latestState, a.config.ValidateTxGasLimit, tx), nil
}
// Simulate runs validation and execution flow of a Tx.
func (a AppManager[T]) Simulate(ctx context.Context, tx T) (appmanager.TxResult, corestore.WriterMap, error) {
_, state, err := a.db.StateLatest()
if err != nil {
return appmanager.TxResult{}, nil, err
}
result, cs := a.stf.Simulate(ctx, state, a.config.SimulationGasLimit, tx) // TODO: check if this is done in the antehandler
return result, cs, nil
}
// Query queries the application at the provided version.
// CONTRACT: Version must always be provided, if 0, get latest
func (a AppManager[T]) Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) {
// if version is provided attempt to do a height query.
if version != 0 {
queryState, err := a.db.StateAt(version)
if err != nil {
return nil, err
}
return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request)
}
// otherwise rely on latest available state.
_, queryState, err := a.db.StateLatest()
if err != nil {
return nil, err
}
return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request)
}
// QueryWithState executes a query with the provided state. This allows to process a query
// independently of the db state. For example, it can be used to process a query with temporary
// and uncommitted state
func (a AppManager[T]) QueryWithState(
ctx context.Context,
state corestore.ReaderMap,
request transaction.Msg,
) (transaction.Msg, error) {
return a.stf.Query(ctx, state, a.config.QueryGasLimit, request)
}