Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x/swingset): include swing-store in genesis #8152

Merged
merged 7 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion golang/cosmos/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ import (

const appName = "agoric"

// FlagSwingStoreExportDir defines the config flag used to specify where a
// genesis swing-store export is expected. For start from genesis, the default
// value is config/swing-store in the home directory. For genesis export, the
// value is always a "swing-store" directory sibling to the exported
// genesis.json file.
// TODO: document this flag in config, likely alongside the genesis path
const FlagSwingStoreExportDir = "swing-store-export-dir"

var (
// DefaultNodeHome default home directories for the application daemon
DefaultNodeHome string
Expand Down Expand Up @@ -588,6 +596,7 @@ func NewAgoricApp(
app.EvidenceKeeper = *evidenceKeeper

skipGenesisInvariants := cast.ToBool(appOpts.Get(crisis.FlagSkipGenesisInvariants))
swingStoreExportDir := cast.ToString(appOpts.Get(FlagSwingStoreExportDir))

// NOTE: Any module instantiated in the module manager that is later modified
// must be passed by reference here.
Expand Down Expand Up @@ -617,7 +626,7 @@ func NewAgoricApp(
transferModule,
icaModule,
vstorage.NewAppModule(app.VstorageKeeper),
swingset.NewAppModule(app.SwingSetKeeper, setBootstrapNeeded, app.ensureControllerInited),
swingset.NewAppModule(app.SwingSetKeeper, &app.SwingStoreExportsHandler, setBootstrapNeeded, app.ensureControllerInited, swingStoreExportDir),
vibcModule,
vbankModule,
lienModule,
Expand Down
7 changes: 5 additions & 2 deletions golang/cosmos/cmd/agd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

func main() {
// We need to delegate to our default app for running the actual chain.
daemoncmd.OnStartHook = func(logger log.Logger) {
launchVM := func(logger log.Logger) {
args := []string{"ag-chain-cosmos", "--home", gaia.DefaultNodeHome}
args = append(args, os.Args[1:]...)

Expand All @@ -22,12 +22,15 @@ func main() {
panic(lookErr)
}

logger.Info("Start chain delegating to JS executable", "binary", binary, "args", args)
logger.Info("agd delegating to JS executable", "binary", binary, "args", args)
execErr := syscall.Exec(binary, args, os.Environ())
if execErr != nil {
panic(execErr)
}
}

daemoncmd.OnStartHook = launchVM
daemoncmd.OnExportHook = launchVM
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some log output in launchVM that could probably be improved for export.


daemon.RunWithController(nil)
}
96 changes: 93 additions & 3 deletions golang/cosmos/daemon/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
genutilcli "github.com/cosmos/cosmos-sdk/x/genutil/client/cli"
"github.com/spf13/cast"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think viper provides enough functionality to justify its use. However, I see it's already used in cosmos-sdk, so consistency with existing code trumps my concerns.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is just imported so I can cast to the Viper instance that cosmos-sdk creates and that's underlying the appOpts

tmcli "github.com/tendermint/tendermint/libs/cli"
"github.com/tendermint/tendermint/libs/log"
dbm "github.com/tendermint/tm-db"
Expand All @@ -40,7 +41,8 @@ import (
type Sender func(needReply bool, str string) (string, error)

var AppName = "agd"
var OnStartHook func(log.Logger)
var OnStartHook func(logger log.Logger)
var OnExportHook func(logger log.Logger)

// NewRootCmd creates a new root command for simd. It is called once in the
// main function.
Expand Down Expand Up @@ -133,6 +135,14 @@ func initRootCmd(sender Sender, rootCmd *cobra.Command, encodingConfig params.En
}
server.AddCommands(rootCmd, gaia.DefaultNodeHome, ac.newApp, ac.appExport, addModuleInitFlags)

hasVMController := sender != nil
for _, command := range rootCmd.Commands() {
if command.Name() == "export" {
extendCosmosExportCommand(command, hasVMController)
break
}
}

// add keybase, auxiliary RPC, query, and tx child commands
rootCmd.AddCommand(
rpc.StatusCommand(),
Expand Down Expand Up @@ -232,7 +242,16 @@ func (ac appCreator) newApp(
panic(err)
}

snapshotDir := filepath.Join(cast.ToString(appOpts.Get(flags.FlagHome)), "data", "snapshots")
homePath := cast.ToString(appOpts.Get(flags.FlagHome))
mhofman marked this conversation as resolved.
Show resolved Hide resolved

// Set a default value for FlagSwingStoreExportDir based on the homePath
// in case we need to InitGenesis with swing-store data
viper, ok := appOpts.(*viper.Viper)
if ok && cast.ToString(appOpts.Get(gaia.FlagSwingStoreExportDir)) == "" {
viper.Set(gaia.FlagSwingStoreExportDir, filepath.Join(homePath, "config", ExportedSwingStoreDirectoryName))
}

snapshotDir := filepath.Join(homePath, "data", "snapshots")
snapshotDB, err := sdk.NewLevelDB("metadata", snapshotDir)
if err != nil {
panic(err)
Expand All @@ -245,7 +264,7 @@ func (ac appCreator) newApp(
return gaia.NewAgoricApp(
ac.sender,
logger, db, traceStore, true, skipUpgradeHeights,
cast.ToString(appOpts.Get(flags.FlagHome)),
homePath,
cast.ToUint(appOpts.Get(server.FlagInvCheckPeriod)),
ac.encCfg,
appOpts,
Expand All @@ -263,6 +282,74 @@ func (ac appCreator) newApp(
)
}

const (
// FlagExportDir is the command-line flag for the "export" command specifying
// where the output of the export should be placed. It contains both the
// items names below: the genesis file, and a directory containing the
// exported swing-store artifacts
FlagExportDir = "export-dir"
// ExportedGenesisFileName is the file name used to save the genesis in the export-dir
ExportedGenesisFileName = "genesis.json"
// ExportedSwingStoreDirectoryName is the directory name used to save the swing-store
// export (artifacts only) in the export-dir
ExportedSwingStoreDirectoryName = "swing-store"
)

// extendCosmosExportCommand monkey-patches the "export" command added by
// cosmos-sdk to add a required "export-dir" command-line flag, and create the
// genesis export in the specified directory.
func extendCosmosExportCommand(cmd *cobra.Command, hasVMController bool) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document, briefly, what this function should do.

cmd.Flags().String(FlagExportDir, "", "The directory where to create the genesis export")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add note in the flag help string explaining difference from the swing store export dir flag, and in that one's help string explaining the difference from this one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add a help string for FlagSwingStoreExportDir as from what I understand it could technically be set by config as well, not just by the command line. I have to admit I'm a little fuzzy with how the whole config thing is plumbed in cosmos-sdk.

Actually I realize now that I didn't actually declare it as an official flag anywhere, I just abused the flag mechanism to plumb through an app option. Where do you recommend I declare the FlagSwingStoreExportDir flag?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second the confusion, but FlagSwingStoreExportDir seems to have strange wiring. At minimum I would look for doc comments.

err := cmd.MarkFlagRequired(FlagExportDir)
if err != nil {
panic(err)
}

originalRunE := cmd.RunE

extendedRunE := func(cmd *cobra.Command, args []string) error {
serverCtx := server.GetServerContextFromCmd(cmd)

exportDir, _ := cmd.Flags().GetString(FlagExportDir)
err := os.MkdirAll(exportDir, os.ModePerm)
if err != nil {
return err
}

genesisPath := filepath.Join(exportDir, ExportedGenesisFileName)
swingStoreExportPath := filepath.Join(exportDir, ExportedSwingStoreDirectoryName)

err = os.MkdirAll(swingStoreExportPath, os.ModePerm)
if err != nil {
return err
}
// We unconditionally set FlagSwingStoreExportDir as for export, it makes
// little sense for users to control this location separately, and we don't
// want to override any swing-store artifacts that may be associated to the
// current genesis.
serverCtx.Viper.Set(gaia.FlagSwingStoreExportDir, swingStoreExportPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I've discerned is that agd export --export-dir=foo is a required flag, but --swing-store-export-dir=foo/something can be used to override where the swingstore is exported. Is that true? In either case, could you add this in comments near FlagExportDir and FlagSwingStoreExportDir?

Copy link
Member Author

@mhofman mhofman Aug 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlagSwingStoreExportDir can only be used to set where to read the swing-store artifacts from for InitGenesis, not for export

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do they need to be different? It seems like the purpose is aligned.


// This will fail is a genesis.json already exists in the export-dir
genesisFile, err := os.OpenFile(genesisPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, os.ModePerm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will catch a preexisting genesis file, but is there also a potential issue if swingStoreExportPath was already created? I suspect not because it would be empty if this line succeeds, but it's worth confirming.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the export-manifest.json file exists in the swingStoreExportPath, the JS export process will fail. Directories existing are not a problem.

if err != nil {
return err
}
defer genesisFile.Close()

cmd.SetOut(genesisFile)

return originalRunE(cmd, args)
}

// Only modify the command handler when we have a VM controller to handle
// the full export logic. Otherwise, appExport will just exec the VM program
// (OnExportHook), which will result in re-entering this flow with the VM
// controller set.
if hasVMController {
cmd.RunE = extendedRunE
}
}

func (ac appCreator) appExport(
logger log.Logger,
db dbm.DB,
Expand All @@ -272,6 +359,9 @@ func (ac appCreator) appExport(
jailAllowedAddrs []string,
appOpts servertypes.AppOptions,
) (servertypes.ExportedApp, error) {
if OnExportHook != nil {
OnExportHook(logger)
}

homePath, ok := appOpts.Get(flags.FlagHome).(string)
if !ok || homePath == "" {
Expand Down
103 changes: 88 additions & 15 deletions golang/cosmos/x/swingset/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
// "os"
"fmt"

agoric "github.com/Agoric/agoric-sdk/golang/cosmos/types"
"github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/keeper"
"github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
Expand All @@ -28,33 +30,58 @@ func DefaultGenesisState() *types.GenesisState {

// InitGenesis initializes the (Cosmos-side) SwingSet state from the GenesisState.
// Returns whether the app should send a bootstrap action to the controller.
func InitGenesis(ctx sdk.Context, keeper Keeper, data *types.GenesisState) bool {
keeper.SetParams(ctx, data.GetParams())
keeper.SetState(ctx, data.GetState())
func InitGenesis(ctx sdk.Context, k Keeper, swingStoreExportsHandler *SwingStoreExportsHandler, swingStoreExportDir string, data *types.GenesisState) bool {
k.SetParams(ctx, data.GetParams())
k.SetState(ctx, data.GetState())

swingStoreExportData := data.GetSwingStoreExportData()
if len(swingStoreExportData) > 0 {
// See https://github.com/Agoric/agoric-sdk/issues/6527
panic("genesis with swing-store state not implemented")
if len(swingStoreExportData) == 0 {
return true
}

// TODO: bootstrap only if not restoring swing-store from genesis state
return true
artifactProvider, err := keeper.OpenSwingStoreExportDirectory(swingStoreExportDir)
if err != nil {
panic(err)
}

swingStore := k.GetSwingStore(ctx)

for _, entry := range swingStoreExportData {
swingStore.Set([]byte(entry.Key), []byte(entry.Value))
}

snapshotHeight := uint64(ctx.BlockHeight())

getExportDataReader := func() (agoric.KVEntryReader, error) {
exportDataIterator := swingStore.Iterator(nil, nil)
return agoric.NewKVIteratorReader(exportDataIterator), nil
}

err = swingStoreExportsHandler.RestoreExport(
keeper.SwingStoreExportProvider{
BlockHeight: snapshotHeight,
GetExportDataReader: getExportDataReader,
ReadNextArtifact: artifactProvider.ReadNextArtifact,
},
keeper.SwingStoreRestoreOptions{
ArtifactMode: keeper.SwingStoreArtifactModeReplay,
ExportDataMode: keeper.SwingStoreExportDataModeAll,
},
)
if err != nil {
panic(err)
}

return false
}

func ExportGenesis(ctx sdk.Context, k Keeper) *types.GenesisState {
func ExportGenesis(ctx sdk.Context, k Keeper, swingStoreExportsHandler *SwingStoreExportsHandler, swingStoreExportDir string) *types.GenesisState {
gs := &types.GenesisState{
Params: k.GetParams(ctx),
State: k.GetState(ctx),
SwingStoreExportData: []*types.SwingStoreExportDataEntry{},
}

// Only export the swing-store shadow copy for now
// TODO:
// - perform state-sync export with check blockHeight (figure out how to
// handle export of historical height),
// - include swing-store artifacts in genesis state
// See https://github.com/Agoric/agoric-sdk/issues/6527
exportDataIterator := k.GetSwingStore(ctx).Iterator(nil, nil)
defer exportDataIterator.Close()
for ; exportDataIterator.Valid(); exportDataIterator.Next() {
Expand All @@ -64,5 +91,51 @@ func ExportGenesis(ctx sdk.Context, k Keeper) *types.GenesisState {
}
gs.SwingStoreExportData = append(gs.SwingStoreExportData, &entry)
}

snapshotHeight := uint64(ctx.BlockHeight())

err := swingStoreExportsHandler.InitiateExport(
// The export will fail if the export of a historical height was requested
snapshotHeight,
swingStoreGenesisEventHandler{exportDir: swingStoreExportDir, snapshotHeight: snapshotHeight},
// The export will fail if the swing-store does not contain all replay artifacts
keeper.SwingStoreExportOptions{
ArtifactMode: keeper.SwingStoreArtifactModeReplay,
ExportDataMode: keeper.SwingStoreExportDataModeSkip,
},
)
if err != nil {
panic(err)
}

err = keeper.WaitUntilSwingStoreExportDone()
if err != nil {
panic(err)
}

return gs
}

type swingStoreGenesisEventHandler struct {
exportDir string
snapshotHeight uint64
}

func (eventHandler swingStoreGenesisEventHandler) OnExportStarted(height uint64, retrieveSwingStoreExport func() error) error {
return retrieveSwingStoreExport()
}

func (eventHandler swingStoreGenesisEventHandler) OnExportRetrieved(provider keeper.SwingStoreExportProvider) error {
if eventHandler.snapshotHeight != provider.BlockHeight {
return fmt.Errorf("snapshot block height (%d) doesn't match requested height (%d)", provider.BlockHeight, eventHandler.snapshotHeight)
}

artifactsProvider := keeper.SwingStoreExportProvider{
GetExportDataReader: func() (agoric.KVEntryReader, error) {
return nil, nil
},
ReadNextArtifact: provider.ReadNextArtifact,
}

return keeper.WriteSwingStoreExportToDirectory(artifactsProvider, eventHandler.exportDir)
}
Loading
Loading