diff --git a/golang/cosmos/app/app.go b/golang/cosmos/app/app.go index 0c7bcf41d69..83b1fb0bdff 100644 --- a/golang/cosmos/app/app.go +++ b/golang/cosmos/app/app.go @@ -44,7 +44,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/capability" capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" - crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" distr "github.com/cosmos/cosmos-sdk/x/distribution" distrclient "github.com/cosmos/cosmos-sdk/x/distribution/client" distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper" @@ -87,14 +86,14 @@ import ( icahostkeeper "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/host/keeper" icahosttypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v6/modules/apps/27-interchain-accounts/types" - "github.com/cosmos/ibc-go/v6/modules/apps/transfer" + ibctransfer "github.com/cosmos/ibc-go/v6/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v6/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" ibc "github.com/cosmos/ibc-go/v6/modules/core" ibcclient "github.com/cosmos/ibc-go/v6/modules/core/02-client" ibcclientclient "github.com/cosmos/ibc-go/v6/modules/core/02-client/client" ibcclienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" - porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + ibcporttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" ibchost "github.com/cosmos/ibc-go/v6/modules/core/24-host" ibckeeper "github.com/cosmos/ibc-go/v6/modules/core/keeper" "github.com/gorilla/mux" @@ -121,6 +120,9 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc" "github.com/Agoric/agoric-sdk/golang/cosmos/x/vlocalchain" "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage" + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer" + vtransferkeeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/keeper" + testtypes "github.com/cosmos/ibc-go/v6/testing/types" // Import the packet forward middleware packetforward "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6/packetforward" @@ -172,7 +174,7 @@ var ( ibc.AppModuleBasic{}, upgrade.AppModuleBasic{}, evidence.AppModuleBasic{}, - transfer.AppModuleBasic{}, + ibctransfer.AppModuleBasic{}, vesting.AppModuleBasic{}, ica.AppModuleBasic{}, packetforward.AppModuleBasic{}, @@ -180,6 +182,7 @@ var ( vstorage.AppModuleBasic{}, vibc.AppModuleBasic{}, vbank.AppModuleBasic{}, + vtransfer.AppModuleBasic{}, ) // module account permissions @@ -220,6 +223,7 @@ type GaiaApp struct { // nolint: golint vibcPort int vstoragePort int vlocalchainPort int + vtransferPort int upgradeDetails *upgradeDetails @@ -260,6 +264,7 @@ type GaiaApp struct { // nolint: golint VibcKeeper vibc.Keeper VbankKeeper vbank.Keeper VlocalchainKeeper vlocalchain.Keeper + VtransferKeeper vtransferkeeper.Keeper // make scoped keepers public for test purposes ScopedIBCKeeper capabilitykeeper.ScopedKeeper @@ -296,9 +301,8 @@ func NewGaiaApp( appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp), ) *GaiaApp { - defaultController := func(ctx context.Context, needReply bool, str string) (string, error) { - fmt.Fprintln(os.Stderr, "FIXME: Would upcall to controller with", str) - return "", nil + var defaultController vm.Sender = func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { + return "", fmt.Errorf("unexpected VM upcall with no controller: %s", jsonRequest) } return NewAgoricApp( defaultController, vm.NewAgdServer(), @@ -308,7 +312,7 @@ func NewGaiaApp( } func NewAgoricApp( - sendToController func(context.Context, bool, string) (string, error), agdServer *vm.AgdServer, + sendToController vm.Sender, agdServer *vm.AgdServer, logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, skipUpgradeHeights map[int64]bool, homePath string, invCheckPeriod uint, encodingConfig gaiaappparams.EncodingConfig, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp), ) *GaiaApp { @@ -327,7 +331,8 @@ func NewAgoricApp( govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey, evidencetypes.StoreKey, ibctransfertypes.StoreKey, packetforwardtypes.StoreKey, capabilitytypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey, icahosttypes.StoreKey, - swingset.StoreKey, vstorage.StoreKey, vibc.StoreKey, vlocalchain.StoreKey, vbank.StoreKey, + swingset.StoreKey, vstorage.StoreKey, vibc.StoreKey, + vlocalchain.StoreKey, vtransfer.StoreKey, vbank.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) @@ -450,11 +455,11 @@ func NewAgoricApp( ) // This function is tricky to get right, so we build it ourselves. - callToController := func(ctx sdk.Context, str string) (string, error) { + callToController := func(ctx sdk.Context, jsonRequest string) (jsonReply string, err error) { app.CheckControllerInited(true) // We use SwingSet-level metering to charge the user for the call. defer app.AgdServer.SetControllerContext(ctx)() - return sendToController(sdk.WrapSDKContext(ctx), true, str) + return sendToController(sdk.WrapSDKContext(ctx), true, jsonRequest) } setBootstrapNeeded := func() { @@ -514,6 +519,19 @@ func NewAgoricApp( vibcIBCModule := vibc.NewIBCModule(app.VibcKeeper) app.vibcPort = app.AgdServer.MustRegisterPortHandler("vibc", vibc.NewReceiver(app.VibcKeeper)) + app.VtransferKeeper = vtransferkeeper.NewKeeper( + appCodec, + keys[vtransfer.StoreKey], + app.VibcKeeper, + scopedTransferKeeper, + app.SwingSetKeeper.PushAction, + ) + + vtransferModule := vtransfer.NewAppModule(app.VtransferKeeper) + app.vtransferPort = app.AgdServer.MustRegisterPortHandler("vtransfer", + vibc.NewReceiver(app.VtransferKeeper), + ) + app.VbankKeeper = vbank.NewKeeper( appCodec, keys[vbank.StoreKey], app.GetSubspace(vbank.ModuleName), app.AccountKeeper, app.BankKeeper, authtypes.FeeCollectorName, @@ -555,56 +573,64 @@ func NewAgoricApp( app.IBCKeeper.ChannelKeeper, app.DistrKeeper, app.BankKeeper, - app.IBCKeeper.ChannelKeeper, + // Make vtransfer the middleware wrapper for the IBCKeeper. + app.VtransferKeeper.GetICS4Wrapper(), ) app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, keys[ibctransfertypes.StoreKey], app.GetSubspace(ibctransfertypes.ModuleName), - app.PacketForwardKeeper, + app.PacketForwardKeeper, // Wire in the middleware ICS4Wrapper. app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.AccountKeeper, app.BankKeeper, scopedTransferKeeper, ) - transferModule := transfer.NewAppModule(app.TransferKeeper) - transferIBCModule := transfer.NewIBCModule(app.TransferKeeper) + app.PacketForwardKeeper.SetTransferKeeper(app.TransferKeeper) - transferPFMModule := packetforward.NewIBCMiddleware( - transferIBCModule, - app.PacketForwardKeeper, - 0, // retries on timeout - packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp, // forward timeout - packetforwardkeeper.DefaultRefundTransferPacketTimeoutTimestamp, // refund timeout - ) + // NewAppModule uses a pointer to the host keeper in case there's a need to + // tie a circular knot with IBC middleware before icahostkeeper.NewKeeper + // can be called. app.ICAHostKeeper = icahostkeeper.NewKeeper( appCodec, keys[icahosttypes.StoreKey], app.GetSubspace(icahosttypes.SubModuleName), - app.PacketForwardKeeper, + app.IBCKeeper.ChannelKeeper, // This is where middleware binding would happen. app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.AccountKeeper, scopedICAHostKeeper, app.MsgServiceRouter(), ) - icaModule := ica.NewAppModule(nil, &app.ICAHostKeeper) icaHostIBCModule := icahost.NewIBCModule(app.ICAHostKeeper) + icaModule := ica.NewAppModule(nil, &app.ICAHostKeeper) + + ics20TransferModule := ibctransfer.NewAppModule(app.TransferKeeper) + + // Create the IBC router, which maps *module names* (not PortIDs) to modules. + ibcRouter := ibcporttypes.NewRouter() - // create static IBC router, add transfer route, then set and seal it - // Don't be confused by the name! The port router maps *module names* (not - // PortIDs) to modules. - ibcRouter := porttypes.NewRouter() + // Add an IBC route for the ICA Host. + ibcRouter.AddRoute(icahosttypes.SubModuleName, icaHostIBCModule) - // transfer stack contains (from top to bottom): - // - ICA Host - // - Packet Forward Middleware wrapping transfer IBC - // - vIBC - ibcRouter.AddRoute(icahosttypes.SubModuleName, icaHostIBCModule). - AddRoute(ibctransfertypes.ModuleName, transferPFMModule). - AddRoute(vibc.ModuleName, vibcIBCModule) + // Add an IBC route for vIBC. + ibcRouter.AddRoute(vibc.ModuleName, vibcIBCModule) + + // Add an IBC route for ICS-20 fungible token transfers, wrapping base + // Cosmos functionality with middleware (from the inside out, Cosmos + // packet-forwarding and then our own "vtransfer"). + var ics20TransferIBCModule ibcporttypes.IBCModule = ibctransfer.NewIBCModule(app.TransferKeeper) + ics20TransferIBCModule = packetforward.NewIBCMiddleware( + ics20TransferIBCModule, + app.PacketForwardKeeper, + 0, // retries on timeout + packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp, // forward timeout + packetforwardkeeper.DefaultRefundTransferPacketTimeoutTimestamp, // refund timeout + ) + ics20TransferIBCModule = vtransfer.NewIBCMiddleware(ics20TransferIBCModule, app.VtransferKeeper) + ibcRouter.AddRoute(ibctransfertypes.ModuleName, ics20TransferIBCModule) // Seal the router app.IBCKeeper.SetRouter(ibcRouter) @@ -658,13 +684,14 @@ func NewAgoricApp( authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), ibc.NewAppModule(app.IBCKeeper), params.NewAppModule(app.ParamsKeeper), - transferModule, + ics20TransferModule, icaModule, packetforward.NewAppModule(app.PacketForwardKeeper), vstorage.NewAppModule(app.VstorageKeeper), swingset.NewAppModule(app.SwingSetKeeper, &app.SwingStoreExportsHandler, setBootstrapNeeded, app.ensureControllerInited, swingStoreExportDir), vibcModule, vbankModule, + vtransferModule, ) // During begin block slashing happens after distr.BeginBlocker so that @@ -673,11 +700,15 @@ func NewAgoricApp( // NOTE: staking module is required if HistoricalEntries param > 0 // NOTE: capability module's beginblocker must come before any modules using capabilities (e.g. IBC) app.mm.SetOrderBeginBlockers( + // Cosmos-SDK modules appear roughly in the order used by simapp and gaiad. // upgrades should be run first upgradetypes.ModuleName, capabilitytypes.ModuleName, + // params influence many other modules, so it should be near the top. + paramstypes.ModuleName, govtypes.ModuleName, stakingtypes.ModuleName, + // ibc apps are grouped together ibctransfertypes.ModuleName, ibchost.ModuleName, icatypes.ModuleName, @@ -691,7 +722,6 @@ func NewAgoricApp( evidencetypes.ModuleName, authz.ModuleName, feegrant.ModuleName, - paramstypes.ModuleName, vestingtypes.ModuleName, vstorage.ModuleName, // This will cause the swingset controller to init if it hadn't yet, passing @@ -699,12 +729,15 @@ func NewAgoricApp( swingset.ModuleName, vibc.ModuleName, vbank.ModuleName, + vtransfer.ModuleName, ) app.mm.SetOrderEndBlockers( - vibc.ModuleName, - vbank.ModuleName, + // Cosmos-SDK modules appear roughly in the order used by simapp and gaiad. govtypes.ModuleName, stakingtypes.ModuleName, + // vibc is an Agoric-specific IBC app, so group it here with other IBC apps. + vibc.ModuleName, + vtransfer.ModuleName, ibctransfertypes.ModuleName, ibchost.ModuleName, icatypes.ModuleName, @@ -722,7 +755,11 @@ func NewAgoricApp( paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, - // SwingSet needs to be last, for it to capture all the pushed actions. + // Putting vbank before SwingSet VM will enable vbank to capture all event + // history that was produced all the other modules, and push those balance + // changes on the VM's actionQueue. + vbank.ModuleName, + // SwingSet VM needs to be last, for it to capture all the pushed actions. swingset.ModuleName, // And then vstorage, to produce SwingSet-induced events. vstorage.ModuleName, @@ -739,26 +776,28 @@ func NewAgoricApp( capabilitytypes.ModuleName, authtypes.ModuleName, banktypes.ModuleName, + paramstypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, ibctransfertypes.ModuleName, + packetforwardtypes.ModuleName, ibchost.ModuleName, icatypes.ModuleName, evidencetypes.ModuleName, feegrant.ModuleName, authz.ModuleName, genutiltypes.ModuleName, - paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, + // Agoric-specific modules go last since they may rely on other SDK modules. vstorage.ModuleName, vbank.ModuleName, vibc.ModuleName, + vtransfer.ModuleName, swingset.ModuleName, - packetforwardtypes.ModuleName, } app.mm.SetOrderInitGenesis(moduleOrderForGenesisAndUpgrade...) @@ -787,7 +826,7 @@ func NewAgoricApp( params.NewAppModule(app.ParamsKeeper), evidence.NewAppModule(app.EvidenceKeeper), ibc.NewAppModule(app.IBCKeeper), - transferModule, + ics20TransferModule, ) app.sm.RegisterStoreDecoders() @@ -837,10 +876,10 @@ func NewAgoricApp( Added: []string{ packetforwardtypes.ModuleName, // Added PFM vlocalchain.ModuleName, // Agoric added vlocalchain + vtransfer.ModuleName, // Agoric added vtransfer }, Deleted: []string{ - crisistypes.ModuleName, // The SDK discontinued the crisis module in v0.51.0 - "lien", // Agoric removed the lien module + "lien", // Agoric removed the lien module }, } @@ -904,6 +943,7 @@ func unreleasedUpgradeHandler(app *GaiaApp, targetUpgrade string) func(sdk.Conte vm.CoreProposalStepForModules( "@agoric/builders/scripts/vats/init-network.js", "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js", ), // Add new vats for price feeds. The existing ones will be retired shortly. vm.CoreProposalStepForModules( @@ -983,6 +1023,7 @@ type cosmosInitAction struct { VbankPort int `json:"vbankPort"` VibcPort int `json:"vibcPort"` VlocalchainPort int `json:"vlocalchainPort"` + VtransferPort int `json:"vtransferPort"` } // Name returns the name of the App @@ -1018,6 +1059,7 @@ func (app *GaiaApp) initController(ctx sdk.Context, bootstrap bool) { VbankPort: app.vbankPort, VibcPort: app.vibcPort, VlocalchainPort: app.vlocalchainPort, + VtransferPort: app.vtransferPort, } // This uses `BlockingSend` as a friendly wrapper for `sendToController` // @@ -1282,11 +1324,44 @@ func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino paramsKeeper.Subspace(slashingtypes.ModuleName) paramsKeeper.Subspace(govtypes.ModuleName).WithKeyTable(govtypesv1.ParamKeyTable()) paramsKeeper.Subspace(ibctransfertypes.ModuleName) + paramsKeeper.Subspace(packetforwardtypes.ModuleName).WithKeyTable(packetforwardtypes.ParamKeyTable()) paramsKeeper.Subspace(ibchost.ModuleName) paramsKeeper.Subspace(icahosttypes.SubModuleName) - paramsKeeper.Subspace(packetforwardtypes.ModuleName).WithKeyTable(packetforwardtypes.ParamKeyTable()) paramsKeeper.Subspace(swingset.ModuleName) paramsKeeper.Subspace(vbank.ModuleName) return paramsKeeper } + +// TestingApp functions + +// GetBaseApp implements the TestingApp interface. +func (app *GaiaApp) GetBaseApp() *baseapp.BaseApp { + return app.BaseApp +} + +// GetStakingKeeper implements the TestingApp interface. +func (app *GaiaApp) GetStakingKeeper() testtypes.StakingKeeper { + return app.StakingKeeper +} + +// GetIBCKeeper implements the TestingApp interface. +func (app *GaiaApp) GetIBCKeeper() *ibckeeper.Keeper { + return app.IBCKeeper +} + +// GetScopedIBCKeeper implements the TestingApp interface. +func (app *GaiaApp) GetScopedIBCKeeper() capabilitykeeper.ScopedKeeper { + return app.ScopedIBCKeeper +} + +// GetTxConfig implements the TestingApp interface. +func (app *GaiaApp) GetTxConfig() client.TxConfig { + return MakeEncodingConfig().TxConfig +} + +// For testing purposes +func (app *GaiaApp) SetSwingStoreExportDir(dir string) { + module := app.mm.Modules[swingset.ModuleName].(swingset.AppModule) + module.SetSwingStoreExportDir(dir) +} diff --git a/golang/cosmos/cmd/agd/main.go b/golang/cosmos/cmd/agd/main.go index e495830149e..ced2e520a07 100644 --- a/golang/cosmos/cmd/agd/main.go +++ b/golang/cosmos/cmd/agd/main.go @@ -57,12 +57,12 @@ func main() { var shutdown func() error nodePort := 1 - sendToNode := func(ctx context.Context, needReply bool, str string) (string, error) { + var sendToNode vm.Sender = func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { if vmClient == nil { return "", errors.New("sendToVM called without VM client set up") } - if str == "shutdown" { + if jsonRequest == "shutdown" { // We could ask nicely, but don't bother. if shutdown != nil { return "", shutdown() @@ -73,10 +73,10 @@ func main() { msg := vm.Message{ Port: nodePort, NeedsReply: needReply, - Data: str, + Data: jsonRequest, } var reply string - err := vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) + err = vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) return reply, err } diff --git a/golang/cosmos/cmd/libdaemon/main.go b/golang/cosmos/cmd/libdaemon/main.go index 2969f66e7d8..ee2f5363ab1 100644 --- a/golang/cosmos/cmd/libdaemon/main.go +++ b/golang/cosmos/cmd/libdaemon/main.go @@ -37,22 +37,22 @@ var agdServer *vm.AgdServer // ConnectVMClientCodec creates an RPC client codec and a sender to the // in-process implementation of the VM. -func ConnectVMClientCodec(ctx context.Context, nodePort int, sendFunc func(int, int, string)) (*vm.ClientCodec, daemoncmd.Sender) { +func ConnectVMClientCodec(ctx context.Context, nodePort int, sendFunc func(int, int, string)) (*vm.ClientCodec, vm.Sender) { vmClientCodec = vm.NewClientCodec(ctx, sendFunc) vmClient := rpc.NewClientWithCodec(vmClientCodec) - sendToNode := func(ctx context.Context, needReply bool, str string) (string, error) { - if str == "shutdown" { + var sendToNode vm.Sender = func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { + if jsonRequest == "shutdown" { return "", vmClientCodec.Close() } msg := vm.Message{ Port: nodePort, NeedsReply: needReply, - Data: str, + Data: jsonRequest, } var reply string - err := vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) + err = vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) return reply, err } @@ -72,7 +72,7 @@ func RunAgCosmosDaemon(nodePort C.int, toNode C.sendFunc, cosmosArgs []*C.char) panic(err) } - var sendToNode daemoncmd.Sender + var sendToNode vm.Sender sendFunc := func(port int, reply int, str string) { C.invokeSendFunc(toNode, C.int(port), C.int(reply), C.CString(str)) diff --git a/golang/cosmos/daemon/cmd/root.go b/golang/cosmos/daemon/cmd/root.go index 403497d3ee1..55550755207 100644 --- a/golang/cosmos/daemon/cmd/root.go +++ b/golang/cosmos/daemon/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "errors" "io" "os" @@ -42,16 +41,13 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/vm" ) -// Sender is a function that sends a request to the controller. -type Sender func(ctx context.Context, needReply bool, str string) (string, error) - var AppName = "agd" var OnStartHook func(*vm.AgdServer, log.Logger, servertypes.AppOptions) error var OnExportHook func(*vm.AgdServer, log.Logger, servertypes.AppOptions) error // NewRootCmd creates a new root command for simd. It is called once in the // main function. -func NewRootCmd(sender Sender) (*cobra.Command, params.EncodingConfig) { +func NewRootCmd(sender vm.Sender) (*cobra.Command, params.EncodingConfig) { encodingConfig := gaia.MakeEncodingConfig() initClientCtx := client.Context{}. WithCodec(encodingConfig.Marshaler). @@ -125,7 +121,7 @@ func initAppConfig() (string, interface{}) { return serverconfig.DefaultConfigTemplate, *srvCfg } -func initRootCmd(sender Sender, rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { +func initRootCmd(sender vm.Sender, rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { cfg := sdk.GetConfig() cfg.Seal() @@ -252,7 +248,7 @@ func txCommand() *cobra.Command { type appCreator struct { encCfg params.EncodingConfig - sender Sender + sender vm.Sender agdServer *vm.AgdServer } diff --git a/golang/cosmos/daemon/main.go b/golang/cosmos/daemon/main.go index a4d4ea770ff..4ae7e69acaa 100644 --- a/golang/cosmos/daemon/main.go +++ b/golang/cosmos/daemon/main.go @@ -13,12 +13,13 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/agoric" app "github.com/Agoric/agoric-sdk/golang/cosmos/app" "github.com/Agoric/agoric-sdk/golang/cosmos/daemon/cmd" + "github.com/Agoric/agoric-sdk/golang/cosmos/vm" sdk "github.com/cosmos/cosmos-sdk/types" ) // DefaultController is a stub controller. -var DefaultController = func(ctx context.Context, needReply bool, str string) (string, error) { +var DefaultController vm.Sender = func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { return "", fmt.Errorf("Controller not configured; did you mean to use `ag-chain-cosmos` instead?") } @@ -28,7 +29,7 @@ func Run() { } // RunWithController starts the app with a custom upcall handler. -func RunWithController(sendToController cmd.Sender) { +func RunWithController(sendToController vm.Sender) { // Exit on Control-C and kill. // Without this explicitly, ag-chain-cosmos ignores them. sigs := make(chan os.Signal, 1) diff --git a/golang/cosmos/go.mod b/golang/cosmos/go.mod index b16f1dc3c13..1c55e96b032 100644 --- a/golang/cosmos/go.mod +++ b/golang/cosmos/go.mod @@ -192,6 +192,9 @@ replace ( // Pick up an IAVL race fix. github.com/cosmos/iavl => github.com/cosmos/iavl v0.19.7 + // Use a version of ibc-go that is compatible with the above forks. + github.com/cosmos/ibc-go/v6 => github.com/agoric-labs/ibc-go/v6 v6.3.1-alpha.agoric.2 + // use cometbft // Use our fork at least until post-v0.34.14 is released with // https://github.com/tendermint/tendermint/issue/6899 resolved. diff --git a/golang/cosmos/go.sum b/golang/cosmos/go.sum index 149dced91a1..7b5acee60ae 100644 --- a/golang/cosmos/go.sum +++ b/golang/cosmos/go.sum @@ -236,6 +236,8 @@ github.com/agoric-labs/cosmos-sdk v0.46.16-alpha.agoric.2.4 h1:i5IgChQjTyWulV/y5 github.com/agoric-labs/cosmos-sdk v0.46.16-alpha.agoric.2.4/go.mod h1:d7e4h+w7FNBNmE6ysp6duBVuQg67pqMtvsLwpT9ca3E= github.com/agoric-labs/cosmos-sdk/ics23/go v0.8.0-alpha.agoric.1 h1:2jvHI/2d+psWAZy6FQ0vXJCHUtfU3ZbbW+pQFL04arQ= github.com/agoric-labs/cosmos-sdk/ics23/go v0.8.0-alpha.agoric.1/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= +github.com/agoric-labs/ibc-go/v6 v6.3.1-alpha.agoric.2 h1:vEzy4JaExzlWNHV3ZSVXEVZcRE9loEFUjieE2TXwDdI= +github.com/agoric-labs/ibc-go/v6 v6.3.1-alpha.agoric.2/go.mod h1:L1xcBjCLIHN7Wd9j6cAQvZertn56pq+eRGFZjRO5bsY= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -380,8 +382,6 @@ github.com/cosmos/iavl v0.19.7 h1:ij32FaEnwxfEurtK0QKDNhTWFnz6NUmrI5gky/WnoY0= github.com/cosmos/iavl v0.19.7/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6 v6.1.2 h1:Hz4nkpStoXIHrC77CIEyu2mRiN2qysGEZPFRf0fpv7w= github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6 v6.1.2/go.mod h1:Jo934o/sW7fNxuOa/TjCalSalz+1Fd649eLyANaJx8g= -github.com/cosmos/ibc-go/v6 v6.3.1 h1:/5ur3AsmNW8WuOevfODHlaY5Ze236PBNE3vVo9o3fQA= -github.com/cosmos/ibc-go/v6 v6.3.1/go.mod h1:Dm14j9s094bGyCEE8W4fD+2t8IneHv+cz+80Mvwjr1w= github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= github.com/cosmos/keyring v1.2.0/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/cosmos/ledger-cosmos-go v0.12.4 h1:drvWt+GJP7Aiw550yeb3ON/zsrgW0jgh5saFCr7pDnw= diff --git a/golang/cosmos/proto/agoric/vtransfer/genesis.proto b/golang/cosmos/proto/agoric/vtransfer/genesis.proto new file mode 100644 index 00000000000..c6b606580cd --- /dev/null +++ b/golang/cosmos/proto/agoric/vtransfer/genesis.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package agoric.vtransfer; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types"; + +// The initial and exported module state. +message GenesisState { + option (gogoproto.equal) = false; + + // The list of account addresses that are being watched by the VM. + repeated bytes watched_addresses = 1 [ + (gogoproto.casttype) = "github.com/cosmos/cosmos-sdk/types.AccAddress", + (gogoproto.jsontag) = "watched_addresses", + (gogoproto.moretags) = "yaml:\"watched_addresses\"" + ]; +} diff --git a/golang/cosmos/vm/client_test.go b/golang/cosmos/vm/client_test.go index 0f23d4bbeb4..d67c98a6bcb 100644 --- a/golang/cosmos/vm/client_test.go +++ b/golang/cosmos/vm/client_test.go @@ -11,30 +11,28 @@ import ( "github.com/Agoric/agoric-sdk/golang/cosmos/vm" ) -type Sender func(ctx context.Context, needReply bool, str string) (string, error) - type errorWrapper struct { Error string `json:"error"` } // ConnectVMClientCodec creates an RPC client codec and a sender to the // in-process implementation of the VM. -func ConnectVMClientCodec(ctx context.Context, nodePort int, sendFunc func(int, int, string)) (*vm.ClientCodec, Sender) { +func ConnectVMClientCodec(ctx context.Context, nodePort int, sendFunc func(int, int, string)) (*vm.ClientCodec, vm.Sender) { vmClientCodec := vm.NewClientCodec(ctx, sendFunc) vmClient := rpc.NewClientWithCodec(vmClientCodec) - sendToNode := func(ctx context.Context, needReply bool, str string) (string, error) { - if str == "shutdown" { + sendToNode := func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { + if jsonRequest == "shutdown" { return "", vmClientCodec.Close() } msg := vm.Message{ Port: nodePort, NeedsReply: needReply, - Data: str, + Data: jsonRequest, } var reply string - err := vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) + err = vmClient.Call(vm.ReceiveMessageMethod, msg, &reply) return reply, err } @@ -42,7 +40,7 @@ func ConnectVMClientCodec(ctx context.Context, nodePort int, sendFunc func(int, } type Fixture struct { - SendToNode Sender + SendToNode vm.Sender SendToGo func(port int, msgStr string) string ReplyToGo func(replyPort int, isError bool, respStr string) int } diff --git a/golang/cosmos/vm/controller.go b/golang/cosmos/vm/controller.go index da7cb4f1522..2bd9a8da970 100644 --- a/golang/cosmos/vm/controller.go +++ b/golang/cosmos/vm/controller.go @@ -7,6 +7,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) +// Sender makes a request of our associated VM. +type Sender func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) + type ControllerAdmissionMsg interface { sdk.Msg CheckAdmissibility(sdk.Context, interface{}) error diff --git a/golang/cosmos/x/swingset/alias.go b/golang/cosmos/x/swingset/alias.go index 117a284a1c6..46be60bfad0 100644 --- a/golang/cosmos/x/swingset/alias.go +++ b/golang/cosmos/x/swingset/alias.go @@ -24,6 +24,8 @@ type ( Keeper = keeper.Keeper SwingStoreExportsHandler = keeper.SwingStoreExportsHandler ExtensionSnapshotter = keeper.ExtensionSnapshotter + ActionContext = types.ActionContext + InboundQueueRecord = types.InboundQueueRecord Egress = types.Egress MsgDeliverInbound = types.MsgDeliverInbound MsgProvision = types.MsgProvision diff --git a/golang/cosmos/x/swingset/keeper/keeper.go b/golang/cosmos/x/swingset/keeper/keeper.go index 5f383bfb9ca..dcd4790c445 100644 --- a/golang/cosmos/x/swingset/keeper/keeper.go +++ b/golang/cosmos/x/swingset/keeper/keeper.go @@ -51,25 +51,6 @@ const ( swingStoreKeyPrefix = "swingStore." ) -// Contextual information about the message source of an action on an inbound queue. -// This context should be unique per inboundQueueRecord. -type actionContext struct { - // The block height in which the corresponding action was enqueued - BlockHeight int64 `json:"blockHeight"` - // The hash of the cosmos transaction that included the message - // If the action didn't result from a transaction message, a substitute value - // may be used. For example the VBANK_BALANCE_UPDATE actions use `x/vbank`. - TxHash string `json:"txHash"` - // The index of the message within the transaction. If the action didn't - // result from a cosmos transaction, a number should be chosen to make the - // actionContext unique. (for example a counter per block and source module). - MsgIdx int `json:"msgIdx"` -} -type inboundQueueRecord struct { - Action vm.Jsonable `json:"action"` - Context actionContext `json:"context"` -} - // Keeper maintains the link to data vstorage and exposes getter/setter methods for the various parts of the state machine type Keeper struct { storeKey storetypes.StoreKey @@ -144,7 +125,14 @@ func (k Keeper) pushAction(ctx sdk.Context, inboundQueuePath string, action vm.A if !txHashOk || !msgIdxOk { stdlog.Printf("error while extracting context for action %q\n", action) } - record := inboundQueueRecord{Action: action, Context: actionContext{BlockHeight: ctx.BlockHeight(), TxHash: txHash, MsgIdx: msgIdx}} + record := types.InboundQueueRecord{ + Action: action, + Context: types.ActionContext{ + BlockHeight: ctx.BlockHeight(), + TxHash: txHash, + MsgIdx: msgIdx, + }, + } bz, err := json.Marshal(record) if err != nil { return err diff --git a/golang/cosmos/x/swingset/keeper/test_utils.go b/golang/cosmos/x/swingset/keeper/test_utils.go new file mode 100644 index 00000000000..85d5cd17800 --- /dev/null +++ b/golang/cosmos/x/swingset/keeper/test_utils.go @@ -0,0 +1,16 @@ +package keeper + +import ( + "testing" + + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage" +) + +// GetVstorageKeeper returns the vstorage keeper from the swingset keeper +// for testing purposes. +func GetVstorageKeeper(t *testing.T, k Keeper) vstorage.Keeper { + if t == nil { + panic("this function is reserved for testing") + } + return k.vstorageKeeper +} diff --git a/golang/cosmos/x/swingset/module.go b/golang/cosmos/x/swingset/module.go index 84a4e932884..c8c646313f6 100644 --- a/golang/cosmos/x/swingset/module.go +++ b/golang/cosmos/x/swingset/module.go @@ -99,6 +99,11 @@ func (AppModule) Name() string { return ModuleName } +// For testing purposes +func (am *AppModule) SetSwingStoreExportDir(dir string) { + am.swingStoreExportDir = dir +} + func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} func (am AppModule) Route() sdk.Route { @@ -149,9 +154,9 @@ func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.V return []abci.ValidatorUpdate{} } -func (am AppModule) checkSwingStoreExportSetup() { +func (am *AppModule) checkSwingStoreExportSetup() { if am.swingStoreExportDir == "" { - panic(fmt.Errorf("SwingStore export dir not set")) + am.swingStoreExportDir = "/tmp/swingset_export" } } diff --git a/golang/cosmos/x/swingset/testing/queue.go b/golang/cosmos/x/swingset/testing/queue.go new file mode 100644 index 00000000000..86d9dd6399d --- /dev/null +++ b/golang/cosmos/x/swingset/testing/queue.go @@ -0,0 +1,17 @@ +package testing + +import ( + "testing" + + "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/keeper" + vstoragetesting "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/testing" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// GetActionQueueRecords returns the records in the action queue. +// This is a testing utility function. +func GetActionQueueRecords(t *testing.T, ctx sdk.Context, swingsetKeeper keeper.Keeper) ([]string, error) { + vstorageKeeper := keeper.GetVstorageKeeper(t, swingsetKeeper) + actionQueueName := keeper.StoragePathActionQueue + return vstoragetesting.GetQueueItems(ctx, vstorageKeeper, actionQueueName) +} diff --git a/golang/cosmos/x/swingset/types/msgs.go b/golang/cosmos/x/swingset/types/msgs.go index ff7f2513e89..2a2ebeca465 100644 --- a/golang/cosmos/x/swingset/types/msgs.go +++ b/golang/cosmos/x/swingset/types/msgs.go @@ -29,6 +29,25 @@ var ( _ vm.ControllerAdmissionMsg = &MsgWalletSpendAction{} ) +// Contextual information about the message source of an action on an inbound queue. +// This context should be unique per inboundQueueRecord. +type ActionContext struct { + // The block height in which the corresponding action was enqueued + BlockHeight int64 `json:"blockHeight"` + // The hash of the cosmos transaction that included the message + // If the action didn't result from a transaction message, a substitute value + // may be used. For example the VBANK_BALANCE_UPDATE actions use `x/vbank`. + TxHash string `json:"txHash"` + // The index of the message within the transaction. If the action didn't + // result from a cosmos transaction, a number should be chosen to make the + // actionContext unique. (for example a counter per block and source module). + MsgIdx int `json:"msgIdx"` +} +type InboundQueueRecord struct { + Action vm.Jsonable `json:"action"` + Context ActionContext `json:"context"` +} + const ( // bundleUncompressedSizeLimit is the (exclusive) limit on uncompressed bundle size. // We must ensure there is an exclusive int64 limit in order to detect an underflow. diff --git a/golang/cosmos/x/vbank/genesis.go b/golang/cosmos/x/vbank/genesis.go index 4b26cadda09..4b043553443 100644 --- a/golang/cosmos/x/vbank/genesis.go +++ b/golang/cosmos/x/vbank/genesis.go @@ -1,8 +1,6 @@ package vbank import ( - // "fmt" - "fmt" "github.com/Agoric/agoric-sdk/golang/cosmos/x/vbank/types" diff --git a/golang/cosmos/x/vbank/types/msgs.go b/golang/cosmos/x/vbank/types/msgs.go index 2b515fe9a39..a57e27a6512 100644 --- a/golang/cosmos/x/vbank/types/msgs.go +++ b/golang/cosmos/x/vbank/types/msgs.go @@ -1,15 +1,3 @@ package types const RouterKey = ModuleName // this was defined in your key.go file - -type VbankSingleBalanceUpdate struct { - Address string `json:"address"` - Denom string `json:"denom"` - Amount string `json:"amount"` -} - -type VbankBalanceUpdate struct { - Nonce uint64 `json:"nonce"` - Type string `json:"type"` - Updated []VbankSingleBalanceUpdate `json:"updated"` -} diff --git a/golang/cosmos/x/vbank/vbank.go b/golang/cosmos/x/vbank/vbank.go index 5b58ffd63bb..ec1c8f888a5 100644 --- a/golang/cosmos/x/vbank/vbank.go +++ b/golang/cosmos/x/vbank/vbank.go @@ -34,14 +34,14 @@ func NewPortHandler(am AppModule, keeper Keeper) portHandler { } } -type vbankSingleBalanceUpdate struct { +type VbankSingleBalanceUpdate struct { Address string `json:"address"` Denom string `json:"denom"` Amount string `json:"amount"` } // Make vbankManyBalanceUpdates sortable -type vbankManyBalanceUpdates []vbankSingleBalanceUpdate +type vbankManyBalanceUpdates []VbankSingleBalanceUpdate var _ sort.Interface = vbankManyBalanceUpdates{} @@ -67,7 +67,7 @@ func (vbu vbankManyBalanceUpdates) Swap(i int, j int) { vbu[i], vbu[j] = vbu[j], vbu[i] } -type vbankBalanceUpdate struct { +type VbankBalanceUpdate struct { *vm.ActionHeader `actionType:"VBANK_BALANCE_UPDATE"` Nonce uint64 `json:"nonce"` Updated vbankManyBalanceUpdates `json:"updated"` @@ -83,9 +83,9 @@ func getBalanceUpdate(ctx sdk.Context, keeper Keeper, addressToUpdate map[string } nonce := keeper.GetNextSequence(ctx) - event := vbankBalanceUpdate{ + event := VbankBalanceUpdate{ Nonce: nonce, - Updated: make([]vbankSingleBalanceUpdate, 0, nentries), + Updated: make([]VbankSingleBalanceUpdate, 0, nentries), } // Note that Golang randomises the order of iteration, so we have to sort @@ -99,7 +99,7 @@ func getBalanceUpdate(ctx sdk.Context, keeper Keeper, addressToUpdate map[string for _, coin := range coins { // generate an update even when the current balance is zero balance := keeper.GetBalance(ctx, account, coin.Denom) - update := vbankSingleBalanceUpdate{ + update := VbankSingleBalanceUpdate{ Address: address, Denom: coin.Denom, Amount: balance.Amount.String(), diff --git a/golang/cosmos/x/vbank/vbank_test.go b/golang/cosmos/x/vbank/vbank_test.go index 416afd985bc..a308eada914 100644 --- a/golang/cosmos/x/vbank/vbank_test.go +++ b/golang/cosmos/x/vbank/vbank_test.go @@ -71,7 +71,7 @@ func newBalances(opts ...balancesOption) balances { return bal } -func validateBalanceUpdate(vbu vbankBalanceUpdate) error { +func validateBalanceUpdate(vbu VbankBalanceUpdate) error { if vbu.Type != "VBANK_BALANCE_UPDATE" { return fmt.Errorf("bad balance update type: %s", vbu.Type) } @@ -89,7 +89,7 @@ func decodeBalances(encoded []byte) (balances, uint64, error) { if encoded == nil { return nil, 0, nil } - balanceUpdate := vbankBalanceUpdate{} + balanceUpdate := VbankBalanceUpdate{} err := json.Unmarshal(encoded, &balanceUpdate) if err != nil { return nil, 0, err diff --git a/golang/cosmos/x/vibc/alias.go b/golang/cosmos/x/vibc/alias.go index f2241dbd16b..c94f23aaa36 100644 --- a/golang/cosmos/x/vibc/alias.go +++ b/golang/cosmos/x/vibc/alias.go @@ -22,6 +22,6 @@ var ( type ( Keeper = keeper.Keeper - ScopedKeeper = types.ScopedKeeper + ScopedKeeper = types.ScopedKeeper MsgSendPacket = types.MsgSendPacket ) diff --git a/golang/cosmos/x/vibc/types/receiver.go b/golang/cosmos/x/vibc/types/receiver.go index 9dfd43b0957..b9b658c895d 100644 --- a/golang/cosmos/x/vibc/types/receiver.go +++ b/golang/cosmos/x/vibc/types/receiver.go @@ -14,7 +14,7 @@ import ( ) var ( - _ vm.PortHandler = Receiver{} + _ vm.PortHandler = (*Receiver)(nil) _ exported.Acknowledgement = (*rawAcknowledgement)(nil) ) @@ -83,18 +83,28 @@ func (r rawAcknowledgement) Success() bool { return true } -func (ir Receiver) Receive(cctx context.Context, str string) (ret string, err error) { +// Receive implements vm.PortHandler. It unmarshals the string as JSON text +// representing an IBC portMessage object. If the resulting type is +// "IBC_METHOD" it dispatches on method ("sendPacket"/"receiveExecuted"/etc.) +// and calls the corresponding method of the wrapped ReceiverImpl. +// +// Otherwise, it requires the wrapped ReceiverImpl to be a vm.PortHandler +// and delegates to the Receive method of that PortHandler. +func (ir Receiver) Receive(cctx context.Context, jsonRequest string) (jsonReply string, err error) { ctx := sdk.UnwrapSDKContext(cctx) impl := ir.impl msg := new(portMessage) - err = json.Unmarshal([]byte(str), &msg) + err = json.Unmarshal([]byte(jsonRequest), &msg) if err != nil { return "", err } if msg.Type != "IBC_METHOD" { - return "", fmt.Errorf(`channel handler only accepts messages of "type": "IBC_METHOD"`) + if receiver, ok := impl.(vm.PortHandler); ok { + return receiver.Receive(cctx, jsonRequest) + } + return "", fmt.Errorf(`channel handler only accepts messages of "type": "IBC_METHOD"; got %q`, msg.Type) } switch msg.Method { @@ -116,7 +126,7 @@ func (ir Receiver) Receive(cctx context.Context, str string) (ret string, err er packet.Sequence = seq bytes, err := json.Marshal(&packet) if err == nil { - ret = string(bytes) + jsonReply = string(bytes) } } @@ -153,8 +163,8 @@ func (ir Receiver) Receive(cctx context.Context, str string) (ret string, err er err = fmt.Errorf("unrecognized method %s", msg.Method) } - if ret == "" && err == nil { - ret = "true" + if jsonReply == "" && err == nil { + jsonReply = "true" } return } diff --git a/golang/cosmos/x/vstorage/keeper/keeper.go b/golang/cosmos/x/vstorage/keeper/keeper.go index 28c97646f9e..8baa17f28c4 100644 --- a/golang/cosmos/x/vstorage/keeper/keeper.go +++ b/golang/cosmos/x/vstorage/keeper/keeper.go @@ -421,7 +421,7 @@ func (k Keeper) GetNoDataValue() []byte { return types.EncodedNoDataValue } -func (k Keeper) getIntValue(ctx sdk.Context, path string) (sdkmath.Int, error) { +func (k Keeper) GetIntValue(ctx sdk.Context, path string) (sdkmath.Int, error) { indexEntry := k.GetEntry(ctx, path) if !indexEntry.HasValue() { return sdk.NewInt(0), nil @@ -435,11 +435,11 @@ func (k Keeper) getIntValue(ctx sdk.Context, path string) (sdkmath.Int, error) { } func (k Keeper) GetQueueLength(ctx sdk.Context, queuePath string) (sdkmath.Int, error) { - head, err := k.getIntValue(ctx, queuePath+".head") + head, err := k.GetIntValue(ctx, queuePath+".head") if err != nil { return sdkmath.NewInt(0), err } - tail, err := k.getIntValue(ctx, queuePath+".tail") + tail, err := k.GetIntValue(ctx, queuePath+".tail") if err != nil { return sdkmath.NewInt(0), err } @@ -450,12 +450,12 @@ func (k Keeper) GetQueueLength(ctx sdk.Context, queuePath string) (sdkmath.Int, func (k Keeper) PushQueueItem(ctx sdk.Context, queuePath string, value string) error { // Get the current queue tail, defaulting to zero if its vstorage doesn't exist. // The `tail` is the value of the next index to be inserted - tail, err := k.getIntValue(ctx, queuePath+".tail") + tail, err := k.GetIntValue(ctx, queuePath+".tail") if err != nil { return err } - if tail.Equal(MaxSDKInt) { + if tail.GTE(MaxSDKInt) { return errors.New(queuePath + " overflow") } nextTail := tail.Add(sdk.NewInt(1)) diff --git a/golang/cosmos/x/vstorage/testing/queue.go b/golang/cosmos/x/vstorage/testing/queue.go new file mode 100644 index 00000000000..14f52bf0f1c --- /dev/null +++ b/golang/cosmos/x/vstorage/testing/queue.go @@ -0,0 +1,27 @@ +package testing + +import ( + "fmt" + + keeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/vstorage/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func GetQueueItems(ctx sdk.Context, vstorageKeeper keeper.Keeper, queuePath string) ([]string, error) { + head, err := vstorageKeeper.GetIntValue(ctx, queuePath+".head") + if err != nil { + return nil, err + } + tail, err := vstorageKeeper.GetIntValue(ctx, queuePath+".tail") + if err != nil { + return nil, err + } + length := tail.Sub(head).Int64() + values := make([]string, length) + var i int64 + for i = 0; i < length; i++ { + path := fmt.Sprintf("%s.%s", queuePath, head.Add(sdk.NewInt(i)).String()) + values[i] = vstorageKeeper.GetEntry(ctx, path).StringValue() + } + return values, nil +} diff --git a/golang/cosmos/x/vtransfer/alias.go b/golang/cosmos/x/vtransfer/alias.go new file mode 100644 index 00000000000..b0754b74f39 --- /dev/null +++ b/golang/cosmos/x/vtransfer/alias.go @@ -0,0 +1,13 @@ +package vtransfer + +import ( + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/keeper" + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types" +) + +const ( + ModuleName = types.ModuleName + StoreKey = types.StoreKey +) + +type Keeper = keeper.Keeper diff --git a/golang/cosmos/x/vtransfer/genesis.go b/golang/cosmos/x/vtransfer/genesis.go new file mode 100644 index 00000000000..b8dc16ee3f1 --- /dev/null +++ b/golang/cosmos/x/vtransfer/genesis.go @@ -0,0 +1,39 @@ +package vtransfer + +import ( + "fmt" + + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types" + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +func NewGenesisState() *types.GenesisState { + return &types.GenesisState{} +} + +func ValidateGenesis(data *types.GenesisState) error { + if data == nil { + return fmt.Errorf("vtransfer genesis data cannot be nil") + } + return nil +} + +func DefaultGenesisState() *types.GenesisState { + return &types.GenesisState{} +} + +func InitGenesis(ctx sdk.Context, keeper Keeper, data *types.GenesisState) []abci.ValidatorUpdate { + keeper.SetWatchedAddresses(ctx, data.GetWatchedAddresses()) + return []abci.ValidatorUpdate{} +} + +func ExportGenesis(ctx sdk.Context, k Keeper) *types.GenesisState { + var gs types.GenesisState + addresses, err := k.GetWatchedAddresses(ctx) + if err != nil { + panic(err) + } + gs.WatchedAddresses = addresses + return &gs +} diff --git a/golang/cosmos/x/vtransfer/genesis_test.go b/golang/cosmos/x/vtransfer/genesis_test.go new file mode 100644 index 00000000000..7e26f82daba --- /dev/null +++ b/golang/cosmos/x/vtransfer/genesis_test.go @@ -0,0 +1,12 @@ +package vtransfer + +import ( + "testing" +) + +func TestDefaultGenesis(t *testing.T) { + defaultGenesisState := DefaultGenesisState() + if err := ValidateGenesis(defaultGenesisState); err != nil { + t.Errorf("DefaultGenesisState did not validate %v: %e", defaultGenesisState, err) + } +} diff --git a/golang/cosmos/x/vtransfer/handler.go b/golang/cosmos/x/vtransfer/handler.go new file mode 100644 index 00000000000..04e89274ef1 --- /dev/null +++ b/golang/cosmos/x/vtransfer/handler.go @@ -0,0 +1,20 @@ +package vtransfer + +import ( + "fmt" + + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +// NewHandler returns a handler for "vtransfer" type messages. +func NewHandler(keeper keeper.Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { + switch msg := msg.(type) { + default: + errMsg := fmt.Sprintf("Unrecognized vtransfer Msg type: %T", msg) + return nil, sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, errMsg) + } + } +} diff --git a/golang/cosmos/x/vtransfer/ibc_middleware.go b/golang/cosmos/x/vtransfer/ibc_middleware.go new file mode 100644 index 00000000000..2264ae87e5b --- /dev/null +++ b/golang/cosmos/x/vtransfer/ibc_middleware.go @@ -0,0 +1,186 @@ +package vtransfer + +import ( + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/keeper" + sdk "github.com/cosmos/cosmos-sdk/types" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + clienttypes "github.com/cosmos/ibc-go/v6/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + "github.com/cosmos/ibc-go/v6/modules/core/exported" +) + +// IBCMiddleware (https://ibc.cosmos.network/main/ibc/apps/ibcmodule) forwards +// most of its methods to the next layer in the stack (which may be the ibc-go +// transfer application or another middleware), but hooks the packet-related +// methods and sends them to vtransferKeeper for async interception by the +// associated VM: +// +// 1. IBCModule channel handshake callbacks (OnChanOpenInit, OnChanOpenTry, +// OnChanOpenAck, and OnChanOpenConfirm)—handled by the wrapped IBCModule. +// +// 2. IBCModule channel closing callbacks (OnChanCloseInit and +// OnChanCloseConfirm)—handled by the wrapped IBCModule. +// +// 3. IBCModule packet callbacks (OnRecvPacket, OnAcknowledgementPacket, and +// OnTimeoutPacket)—intercepted by vtransfer. +// +// 4. ICS4Wrapper packet initiation methods (SendPacket, WriteAcknowledgement +// and GetAppVersion)—delegated by vtransfer to vibc. + +var _ porttypes.Middleware = (*IBCMiddleware)(nil) + +// IBCMiddleware implements the ICS26 callbacks for the middleware given the +// underlying IBCModule and the keeper. +type IBCMiddleware struct { + ibcModule porttypes.IBCModule + vtransferKeeper keeper.Keeper +} + +// NewIBCMiddleware creates a new IBCMiddleware given the underlying IBCModule and keeper. +func NewIBCMiddleware(ibcModule porttypes.IBCModule, vtransferKeeper keeper.Keeper) IBCMiddleware { + return IBCMiddleware{ + ibcModule: ibcModule, + vtransferKeeper: vtransferKeeper, + } +} + +/////////////////////////////////// +// The following channel handshake events are all directly forwarded to the +// wrapped IBCModule. They are not performed in the context of a packet, and so +// do not need to be intercepted. + +// OnChanCloseInit implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenInit( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID string, + channelID string, + chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + version string, +) (string, error) { + return im.ibcModule.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version) +} + +// OnChanOpenTry implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenTry( + ctx sdk.Context, + order channeltypes.Order, + connectionHops []string, + portID, + channelID string, + chanCap *capabilitytypes.Capability, + counterparty channeltypes.Counterparty, + counterpartyVersion string, +) (string, error) { + return im.ibcModule.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, counterpartyVersion) +} + +// OnChanOpenAck implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenAck( + ctx sdk.Context, + portID, + channelID string, + counterpartyChannelID string, + counterpartyVersion string, +) error { + return im.ibcModule.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) +} + +// OnChanOpenConfirm implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.ibcModule.OnChanOpenConfirm(ctx, portID, channelID) +} + +// OnChanCloseInit implements the IBCModule interface. +func (im IBCMiddleware) OnChanCloseInit( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.ibcModule.OnChanCloseInit(ctx, portID, channelID) +} + +// OnChanCloseConfirm implements the IBCModule interface. +func (im IBCMiddleware) OnChanCloseConfirm( + ctx sdk.Context, + portID, + channelID string, +) error { + return im.ibcModule.OnChanCloseConfirm(ctx, portID, channelID) +} + +/////////////////////////////////// +// The following packet methods are all implemented by +// im.vtransferKeeper.Intercept*, so named because those methods are "tee"s +// combining the middleware stack with an interception of the packet event +// (On*Packet) or packet method (WriteAcknowledgment) by the async VM. + +// OnRecvPacket implements the IBCModule interface. +func (im IBCMiddleware) OnRecvPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) exported.Acknowledgement { + return im.vtransferKeeper.InterceptOnRecvPacket(ctx, im.ibcModule, packet, relayer) +} + +// OnAcknowledgementPacket implements the IBCModule interface. +func (im IBCMiddleware) OnAcknowledgementPacket( + ctx sdk.Context, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + return im.vtransferKeeper.InterceptOnAcknowledgementPacket(ctx, im.ibcModule, packet, acknowledgement, relayer) +} + +// OnTimeoutPacket implements the IBCModule interface. +func (im IBCMiddleware) OnTimeoutPacket( + ctx sdk.Context, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + return im.vtransferKeeper.InterceptOnTimeoutPacket(ctx, im.ibcModule, packet, relayer) +} + +// WriteAcknowledgement implements the ICS4 Wrapper interface. +// Unlike implementations of IBCModule interface methods, implementations of +// ICS4 Wrapper interface methods do not pass along the wrapped IBC module +// because they support packet initiation. +func (im IBCMiddleware) WriteAcknowledgement( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + packet exported.PacketI, + ack exported.Acknowledgement, +) error { + return im.vtransferKeeper.InterceptWriteAcknowledgement(ctx, chanCap, packet, ack) +} + +/////////////////////////////////// +// The following methods are directly implemented by the ICS4Wrapper outside of +// us, whether the ibc-go stack or another middleware. + +// SendPacket implements the ICS4 Wrapper interface. +func (im IBCMiddleware) SendPacket( + ctx sdk.Context, + chanCap *capabilitytypes.Capability, + sourcePort string, + sourceChannel string, + timeoutHeight clienttypes.Height, + timeoutTimestamp uint64, + data []byte, +) (uint64, error) { + return im.vtransferKeeper.SendPacket(ctx, chanCap, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// GetAppVersion implements the ICS4 Wrapper interface. +func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return im.vtransferKeeper.GetAppVersion(ctx, portID, channelID) +} diff --git a/golang/cosmos/x/vtransfer/ibc_middleware_test.go b/golang/cosmos/x/vtransfer/ibc_middleware_test.go new file mode 100644 index 00000000000..ab82e14e09d --- /dev/null +++ b/golang/cosmos/x/vtransfer/ibc_middleware_test.go @@ -0,0 +1,448 @@ +package vtransfer_test + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "strings" + "testing" + "text/template" + + app "github.com/Agoric/agoric-sdk/golang/cosmos/app" + "github.com/Agoric/agoric-sdk/golang/cosmos/vm" + "github.com/cosmos/cosmos-sdk/store" + "github.com/stretchr/testify/suite" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + swingsettesting "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/testing" + swingsettypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types" + vibckeeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/keeper" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + ibctransfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v6/testing" + "github.com/cosmos/ibc-go/v6/testing/simapp" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" +) + +type IntegrationTestSuite struct { + suite.Suite + + coordinator *ibctesting.Coordinator + + // testing chains used for convenience and readability + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain + + queryClient ibctransfertypes.QueryClient +} + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +type TestingAppMaker func() (ibctesting.TestingApp, map[string]json.RawMessage) + +// Each instance has unique IBC genesis state with deterministic +// client/connection/channel initial sequence numbers +// (respectively, X000/X010/X050 where X is the zero-based +// instance number plus one, such that instance 0 uses +// 1000/1010/1050, instance 1 uses 2000/2010/2050, etc.). +func computeSequences(instance int) (clientSeq, connectionSeq, channelSeq int) { + baseSequence := 1000 * (instance + 1) + return baseSequence, baseSequence + 10, baseSequence + 50 +} + +func SetupAgoricTestingApp(instance int) TestingAppMaker { + return func() (ibctesting.TestingApp, map[string]json.RawMessage) { + db := dbm.NewMemDB() + encCdc := app.MakeEncodingConfig() + mockController := func(ctx context.Context, needReply bool, jsonRequest string) (jsonReply string, err error) { + // fmt.Printf("controller %d got: %s\n", instance, jsonRequest) + + // Check that the message is at least JSON. + var jsonAny interface{} + if err := json.Unmarshal([]byte(jsonRequest), &jsonAny); err != nil { + panic(err) + } + + // Our reply must be truthy or else we don't make it past AG_COSMOS_INIT. + jsonReply = `true` + return jsonReply, nil + } + appd := app.NewAgoricApp(mockController, vm.NewAgdServer(), log.TestingLogger(), db, nil, + true, map[int64]bool{}, app.DefaultNodeHome, simapp.FlagPeriodValue, encCdc, simapp.EmptyAppOptions{}, interBlockCacheOpt()) + genesisState := app.NewDefaultGenesisState() + + t := template.Must(template.New("").Parse(` + { + "client_genesis": { + "clients": [], + "clients_consensus": [], + "clients_metadata": [], + "create_localhost": false, + "next_client_sequence": "{{.nextClientSequence}}", + "params": { + "allowed_clients": [ + "06-solomachine", + "07-tendermint" + ] + } + }, + "connection_genesis": { + "client_connection_paths": [], + "connections": [], + "next_connection_sequence": "{{.nextConnectionSequence}}", + "params": { + "max_expected_time_per_block": "30000000000" + } + }, + "channel_genesis": { + "ack_sequences": [], + "acknowledgements": [], + "channels": [], + "commitments": [], + "next_channel_sequence": "{{.nextChannelSequence}}", + "receipts": [], + "recv_sequences": [], + "send_sequences": [] + } + }`)) + var result strings.Builder + clientSeq, connectionSeq, channelSeq := computeSequences(instance) + err := t.Execute(&result, map[string]any{ + "nextClientSequence": clientSeq, + "nextConnectionSequence": connectionSeq, + "nextChannelSequence": channelSeq, + }) + if err != nil { + panic(err) + } + genesisState["ibc"] = json.RawMessage(result.String()) + return appd, genesisState + } +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} + +// SetupTest initializes an IntegrationTestSuite with two similar chains, a +// shared coordinator, and a query client that happens to point at chainA. +func (s *IntegrationTestSuite) SetupTest() { + s.coordinator = ibctesting.NewCoordinator(s.T(), 0) + + chains := make(map[string]*ibctesting.TestChain) + for i := 0; i < 2; i++ { + ibctesting.DefaultTestingAppInit = SetupAgoricTestingApp(i) + + chainID := ibctesting.GetChainID(i) + chain := ibctesting.NewTestChain(s.T(), s.coordinator, chainID) + + balance := banktypes.Balance{ + Address: chain.SenderAccount.GetAddress().String(), + Coins: sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(100000000000000))), + } + + // create application and override files in the IBC test chain + app := ibctesting.SetupWithGenesisValSet( + s.T(), + chain.Vals, + []authtypes.GenesisAccount{ + chain.SenderAccount.(authtypes.GenesisAccount), + }, + chainID, + sdk.DefaultPowerReduction, + balance, + ) + + chain.App = app + chain.QueryServer = app.GetIBCKeeper() + chain.TxConfig = app.GetTxConfig() + chain.Codec = app.AppCodec() + chain.CurrentHeader = tmproto.Header{ + ChainID: chainID, + Height: 1, + Time: s.coordinator.CurrentTime.UTC(), + } + + s.coordinator.CommitBlock(chain) + + chains[chainID] = chain + } + + s.coordinator.Chains = chains + s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(0)) + s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(1)) + + agoricApp := s.GetApp(s.chainA) + + queryHelper := baseapp.NewQueryServerTestHelper(s.chainA.GetContext(), agoricApp.InterfaceRegistry()) + ibctransfertypes.RegisterQueryServer(queryHelper, agoricApp.TransferKeeper) + s.queryClient = ibctransfertypes.NewQueryClient(queryHelper) +} + +func (s *IntegrationTestSuite) GetApp(chain *ibctesting.TestChain) *app.GaiaApp { + app, ok := chain.App.(*app.GaiaApp) + if !ok { + panic("not agoric app") + } + + return app +} + +func (s *IntegrationTestSuite) NewTransferPath() *ibctesting.Path { + path := ibctesting.NewPath(s.chainA, s.chainB) + _, _, channelASeq := computeSequences(0) + _, _, channelBSeq := computeSequences(1) + path.EndpointA.ChannelID = fmt.Sprintf("channel-%d", channelASeq) + path.EndpointB.ChannelID = fmt.Sprintf("channel-%d", channelBSeq) + path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort + path.EndpointA.ChannelConfig.Version = "ics20-1" + path.EndpointB.ChannelConfig.Version = "ics20-1" + + s.coordinator.Setup(path) + + s.coordinator.CommitBlock(s.chainA, s.chainB) + + return path +} + +func (s *IntegrationTestSuite) assertActionQueue(chain *ibctesting.TestChain, expectedRecords []swingsettypes.InboundQueueRecord) { + actualRecords, err := swingsettesting.GetActionQueueRecords( + s.T(), + chain.GetContext(), + s.GetApp(chain).SwingSetKeeper, + ) + s.Require().NoError(err) + + exLen := len(expectedRecords) + recLen := len(actualRecords) + maxLen := exLen + if recLen > maxLen { + maxLen = recLen + } + for i := 0; i < maxLen; i++ { + if i >= recLen { + s.Fail("expected record", "%d: %q", i, expectedRecords[i]) + continue + } else if i >= exLen { + s.Fail("unexpected record", "%d: %v", i, actualRecords[i]) + continue + } + expi := expectedRecords[i] + var reci swingsettypes.InboundQueueRecord + err := json.Unmarshal([]byte(actualRecords[i]), &reci) + s.Require().NoError(err) + + if expi.Context.TxHash == "" { + // Default the TxHash. + expi.Context.TxHash = reci.Context.TxHash + } + + // Comparing unmarshaled values with an inlined object fails. + // So we marshal the expected object and compare the strings. + expbz, err := json.Marshal(expi) + s.Require().NoError(err) + + s.Equal(string(expbz), actualRecords[i]) + } +} + +func (s *IntegrationTestSuite) RegisterBridgeTarget(chain *ibctesting.TestChain, target string) { + agdServer := s.GetApp(chain).AgdServer + defer agdServer.SetControllerContext(chain.GetContext())() + var reply string + err := agdServer.ReceiveMessage( + &vm.Message{ + Port: agdServer.GetPort("vtransfer"), + Data: `{"type":"BRIDGE_TARGET_REGISTER","target":"` + target + `"}`, + }, + &reply, + ) + s.Require().NoError(err) + s.Require().Equal(reply, "true") +} + +func (s *IntegrationTestSuite) TransferFromSourceChain( + srcChain *ibctesting.TestChain, + data ibctransfertypes.FungibleTokenPacketData, + src, dst *ibctesting.Endpoint, +) (channeltypes.Packet, error) { + tokenAmt, ok := sdk.NewIntFromString(data.Amount) + s.Require().True(ok) + + timeoutHeight := srcChain.GetTimeoutHeight() + packet := channeltypes.NewPacket(data.GetBytes(), 0, src.ChannelConfig.PortID, src.ChannelID, dst.ChannelConfig.PortID, dst.ChannelID, timeoutHeight, 0) + + // send a transfer packet from src + imt := ibctransfertypes.MsgTransfer{ + SourcePort: packet.SourcePort, + SourceChannel: packet.SourceChannel, + Memo: data.Memo, + Token: sdk.NewCoin(data.Denom, tokenAmt), + Sender: data.Sender, + Receiver: data.Receiver, + TimeoutHeight: packet.TimeoutHeight, + TimeoutTimestamp: packet.TimeoutTimestamp, + } + imr, err := s.GetApp(srcChain).TransferKeeper.Transfer(srcChain.GetContext(), &imt) + s.Require().NoError(err) + packet.Sequence = imr.Sequence + + return packet, nil +} + +func (s *IntegrationTestSuite) mintToAddress(chain *ibctesting.TestChain, addr sdk.AccAddress, denom, amount string) { + app := s.GetApp(chain) + tokenAmt, ok := sdk.NewIntFromString(amount) + s.Require().True(ok) + intAmt, err := strconv.ParseInt(amount, 10, 64) + s.Require().NoError(err) + coins := sdk.NewCoins(sdk.NewCoin(denom, tokenAmt.Mul(sdk.NewInt(intAmt)))) + err = app.BankKeeper.MintCoins(chain.GetContext(), ibctransfertypes.ModuleName, coins) + s.Require().NoError(err) + err = app.BankKeeper.SendCoinsFromModuleToAccount(chain.GetContext(), ibctransfertypes.ModuleName, addr, coins) + s.Require().NoError(err) + + // Verify success. + balances := app.BankKeeper.GetAllBalances(chain.GetContext(), addr) + s.Require().Equal(coins[0], balances[1]) +} + +// TestTransferFromAgdToAgd relays an IBC transfer initiated from a chain A to a +// chain B, and relays the chain B's resulting acknowledgement in return. It +// verifies that the source and destination accounts' bridge targets are called +// by inspecting their resulting actionQueue records. By committing blocks +// between actions, the test verifies that the VM results are permitted to be +// async across blocks. +func (s *IntegrationTestSuite) TestTransferFromAgdToAgd() { + path := s.NewTransferPath() + s.Require().Equal(path.EndpointA.ChannelID, "channel-1050") + + s.Run("TransferFromAgdToAgd", func() { + // create a transfer packet's data contents + transferData := ibctransfertypes.NewFungibleTokenPacketData( + "uosmo", + "1000000", + s.chainA.SenderAccount.GetAddress().String(), + s.chainB.SenderAccounts[1].SenderAccount.GetAddress().String(), + `"This is a JSON memo"`, + ) + + // Register the sender and receiver as bridge targets on their specific + // chain. + s.RegisterBridgeTarget(s.chainA, transferData.Sender) + s.RegisterBridgeTarget(s.chainB, transferData.Receiver) + + s.mintToAddress(s.chainA, s.chainA.SenderAccount.GetAddress(), transferData.Denom, transferData.Amount) + + // Initiate the transfer + packet, err := s.TransferFromSourceChain(s.chainA, transferData, path.EndpointA, path.EndpointB) + s.Require().NoError(err) + + // Relay the packet + s.coordinator.CommitBlock(s.chainA) + err = path.EndpointB.UpdateClient() + s.Require().NoError(err) + s.coordinator.CommitBlock(s.chainB) + + writeAcknowledgementHeight := s.chainB.CurrentHeader.Height + writeAcknowledgementTime := s.chainB.CurrentHeader.Time.Unix() + + err = path.EndpointB.RecvPacket(packet) + s.Require().NoError(err) + + // Create a success ack as defined by ICS20. + ack := channeltypes.NewResultAcknowledgement([]byte{1}) + // Create a different ack to show that a contract can change it. + contractAck := channeltypes.NewResultAcknowledgement([]byte{5}) + + s.coordinator.CommitBlock(s.chainA, s.chainB) + + { + expectedRecords := []swingsettypes.InboundQueueRecord{} + s.assertActionQueue(s.chainA, expectedRecords) + } + + { + expectedRecords := []swingsettypes.InboundQueueRecord{ + { + Action: &vibckeeper.WriteAcknowledgementEvent{ + ActionHeader: &vm.ActionHeader{ + Type: "VTRANSFER_IBC_EVENT", + BlockHeight: writeAcknowledgementHeight, + BlockTime: writeAcknowledgementTime, + }, + Event: "writeAcknowledgement", + Target: transferData.Receiver, + Packet: packet, + Acknowledgement: ack.Acknowledgement(), + }, + Context: swingsettypes.ActionContext{ + BlockHeight: writeAcknowledgementHeight, + // TxHash is filled in below + MsgIdx: 0, + }, + }, + } + + s.assertActionQueue(s.chainB, expectedRecords) + + // write out a different acknowledgement from the "contract", one block later. + s.coordinator.CommitBlock(s.chainB) + err = s.GetApp(s.chainB).VtransferKeeper.ReceiveWriteAcknowledgement(s.chainB.GetContext(), packet, contractAck) + s.Require().NoError(err) + + s.coordinator.CommitBlock(s.chainB) + } + + // Update Client + err = path.EndpointA.UpdateClient() + s.Require().NoError(err) + + acknowledgementHeight := s.chainA.CurrentHeader.Height + acknowledgementTime := s.chainA.CurrentHeader.Time.Unix() + + // Prove the packet's acknowledgement. + err = path.EndpointA.AcknowledgePacket(packet, contractAck.Acknowledgement()) + s.Require().NoError(err) + + s.coordinator.CommitBlock(s.chainA, s.chainB) + + { + expectedRecords := []swingsettypes.InboundQueueRecord{ + { + Action: &vibckeeper.WriteAcknowledgementEvent{ + ActionHeader: &vm.ActionHeader{ + Type: "VTRANSFER_IBC_EVENT", + BlockHeight: acknowledgementHeight, + BlockTime: acknowledgementTime, + }, + Event: "acknowledgementPacket", + Target: transferData.Sender, + Packet: packet, + Acknowledgement: contractAck.Acknowledgement(), + Relayer: s.chainA.SenderAccount.GetAddress(), + }, + Context: swingsettypes.ActionContext{ + BlockHeight: acknowledgementHeight, + // TxHash is filled in below + MsgIdx: 0, + }, + }, + } + + s.assertActionQueue(s.chainA, expectedRecords) + } + }) +} diff --git a/golang/cosmos/x/vtransfer/keeper/keeper.go b/golang/cosmos/x/vtransfer/keeper/keeper.go new file mode 100644 index 00000000000..36701a544e7 --- /dev/null +++ b/golang/cosmos/x/vtransfer/keeper/keeper.go @@ -0,0 +1,281 @@ +package keeper + +import ( + "context" + "encoding/json" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/store/prefix" + storetypes "github.com/cosmos/cosmos-sdk/store/types" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" + capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + + "github.com/Agoric/agoric-sdk/golang/cosmos/vm" + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc" + vibctypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/types" + transfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v6/modules/core/05-port/types" + host "github.com/cosmos/ibc-go/v6/modules/core/24-host" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" +) + +var _ porttypes.ICS4Wrapper = (*Keeper)(nil) +var _ vibctypes.ReceiverImpl = (*Keeper)(nil) +var _ vm.PortHandler = (*Keeper)(nil) + +// "watched addresses" is logically a set and physically a collection of +// KVStore entries in which each key is a concatenation of a fixed prefix and +// the address, and its corresponding value is a non-empty but otherwise irrelevant +// sentinel. +const ( + watchedAddressStoreKeyPrefix = "watchedAddress/" + watchedAddressSentinel = "y" +) + +// Keeper handles the interceptions from the vtransfer IBC middleware, passing +// them to the embedded vibc keeper if they involve a targeted address (which is +// an address associated with a VM listener). The embedded vibc keeper is used +// to bridge calls to swingset, but with a special wrapper on the bridge +// controller to use distinct action types. The keeper keeps a store of +// "targeted addresses", managed from Swingset by bridge messages. +type Keeper struct { + porttypes.ICS4Wrapper + vibctypes.ReceiverImpl + + vibcKeeper vibc.Keeper + + key storetypes.StoreKey + cdc codec.Codec + + vibcModule porttypes.IBCModule +} + +// NewKeeper creates a new vtransfer Keeper instance +func NewKeeper( + cdc codec.Codec, + key storetypes.StoreKey, + prototypeVibcKeeper vibc.Keeper, + scopedTransferKeeper capabilitykeeper.ScopedKeeper, + pushAction vm.ActionPusher, +) Keeper { + wrappedPushAction := wrapActionPusher(pushAction) + + // This vibcKeeper is used to send notifications from the vtransfer middleware + // to the VM. + vibcKeeper := prototypeVibcKeeper.WithScope(nil, scopedTransferKeeper, wrappedPushAction) + return Keeper{ + ICS4Wrapper: vibcKeeper, + ReceiverImpl: vibcKeeper, + + vibcKeeper: vibcKeeper, + key: key, + vibcModule: vibc.NewIBCModule(vibcKeeper), + cdc: cdc, + } +} + +// wrapActionPusher wraps an ActionPusher to prefix the action type with +// "VTRANSFER_". +func wrapActionPusher(pusher vm.ActionPusher) vm.ActionPusher { + return func(ctx sdk.Context, action vm.Action) error { + action = vm.PopulateAction(ctx, action) + + // Prefix the action type. + ah := action.GetActionHeader() + ah.Type = "VTRANSFER_" + ah.Type + + // fmt.Println("@@@ vtransfer action", action) + return pusher(ctx, action) + } +} + +func (k Keeper) GetICS4Wrapper() porttypes.ICS4Wrapper { + return k +} + +func (k Keeper) GetReceiverImpl() vibctypes.ReceiverImpl { + return k +} + +// InterceptOnRecvPacket runs the ibcModule and eventually acknowledges a packet. +// Many error acknowledgments are sent synchronously, but most cases instead return nil +// to tell the IBC system that acknowledgment is async (i.e., that WriteAcknowledgement +// will be called later, after the VM has dealt with the packet). +func (k Keeper) InterceptOnRecvPacket(ctx sdk.Context, ibcModule porttypes.IBCModule, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + ack := ibcModule.OnRecvPacket(ctx, packet, relayer) + + if ack == nil { + // Already declared to be an async ack. + return nil + } + portID := packet.GetDestPort() + channelID := packet.GetDestChannel() + capName := host.ChannelCapabilityPath(portID, channelID) + chanCap, ok := k.vibcKeeper.GetCapability(ctx, capName) + if !ok { + err := sdkerrors.Wrapf(channeltypes.ErrChannelCapabilityNotFound, "could not retrieve channel capability at: %s", capName) + return channeltypes.NewErrorAcknowledgement(err) + } + // Give the VM a chance to write (or override) the ack. + if err := k.InterceptWriteAcknowledgement(ctx, chanCap, packet, ack); err != nil { + return channeltypes.NewErrorAcknowledgement(err) + } + return nil +} + +// InterceptOnAcknowledgementPacket checks to see if the packet sender is a +// targeted account, and if so, delegates to the VM. +func (k Keeper) InterceptOnAcknowledgementPacket( + ctx sdk.Context, + ibcModule porttypes.IBCModule, + packet channeltypes.Packet, + acknowledgement []byte, + relayer sdk.AccAddress, +) error { + // Pass every acknowledgement to the wrapped IBC module. + modErr := ibcModule.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer) + + // If the sender is not a targeted account, we're done. + sender, _, err := k.parseTransfer(ctx, packet) + if err != nil || sender == "" { + return modErr + } + + // Trigger VM, regardless of errors in the ibcModule. + vmErr := k.vibcKeeper.TriggerOnAcknowledgementPacket(ctx, sender, packet, acknowledgement, relayer) + + // Any error from the VM is trumped by one from the wrapped IBC module. + if modErr != nil { + return modErr + } + return vmErr +} + +// InterceptOnTimeoutPacket checks to see if the packet sender is a targeted +// account, and if so, delegates to the VM. +func (k Keeper) InterceptOnTimeoutPacket( + ctx sdk.Context, + ibcModule porttypes.IBCModule, + packet channeltypes.Packet, + relayer sdk.AccAddress, +) error { + // Pass every timeout to the wrapped IBC module. + modErr := ibcModule.OnTimeoutPacket(ctx, packet, relayer) + + // If the sender is not a targeted account, we're done. + sender, _, err := k.parseTransfer(ctx, packet) + if err != nil || sender == "" { + return modErr + } + + // Trigger VM, regardless of errors in the app. + vmErr := k.vibcKeeper.TriggerOnTimeoutPacket(ctx, sender, packet, relayer) + + // Any error from the VM is trumped by one from the wrapped IBC module. + if modErr != nil { + return modErr + } + return vmErr +} + +// InterceptWriteAcknowledgement checks to see if the packet's receiver is a +// targeted account, and if so, delegates to the VM. +func (k Keeper) InterceptWriteAcknowledgement(ctx sdk.Context, chanCap *capabilitytypes.Capability, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + _, receiver, err := k.parseTransfer(ctx, packet) + if err != nil || receiver == "" { + // We can't parse, but that means just to ack directly. + return k.WriteAcknowledgement(ctx, chanCap, packet, ack) + } + + // Trigger VM + if err = k.vibcKeeper.TriggerWriteAcknowledgement(ctx, receiver, packet, ack); err != nil { + errAck := channeltypes.NewErrorAcknowledgement(err) + return k.WriteAcknowledgement(ctx, chanCap, packet, errAck) + } + + return nil +} + +// parseTransfer checks if a packet's sender and/or receiver are targeted accounts. +func (k Keeper) parseTransfer(ctx sdk.Context, packet ibcexported.PacketI) (string, string, error) { + var transferData transfertypes.FungibleTokenPacketData + err := k.cdc.UnmarshalJSON(packet.GetData(), &transferData) + if err != nil { + return "", "", err + } + + var sender string + var receiver string + prefixStore := prefix.NewStore( + ctx.KVStore(k.key), + []byte(watchedAddressStoreKeyPrefix), + ) + if prefixStore.Has([]byte(transferData.Sender)) { + sender = transferData.Sender + } + if prefixStore.Has([]byte(transferData.Receiver)) { + receiver = transferData.Receiver + } + return sender, receiver, nil +} + +// GetWatchedAdresses returns the watched addresses from the keeper as a slice +// of account addresses. +func (k Keeper) GetWatchedAddresses(ctx sdk.Context) ([]sdk.AccAddress, error) { + addresses := make([]sdk.AccAddress, 0) + prefixStore := prefix.NewStore(ctx.KVStore(k.key), []byte(watchedAddressStoreKeyPrefix)) + iterator := sdk.KVStorePrefixIterator(prefixStore, []byte{}) + defer iterator.Close() + for ; iterator.Valid(); iterator.Next() { + addr, err := sdk.AccAddressFromBech32(string(iterator.Key())) + if err != nil { + return nil, err + } + addresses = append(addresses, addr) + } + return addresses, nil +} + +// SetWatchedAddresses sets the watched addresses in the keeper from a slice of +// SDK account addresses. +func (k Keeper) SetWatchedAddresses(ctx sdk.Context, addresses []sdk.AccAddress) { + prefixStore := prefix.NewStore( + ctx.KVStore(k.key), + []byte(watchedAddressStoreKeyPrefix), + ) + for _, addr := range addresses { + prefixStore.Set([]byte(addr.String()), []byte(watchedAddressSentinel)) + } +} + +type registrationAction struct { + Type string `json:"type"` // BRIDGE_TARGET_REGISTER or BRIDGE_TARGET_UNREGISTER + Target string `json:"target"` +} + +// Receive implements the vm.PortHandler interface. +func (k Keeper) Receive(cctx context.Context, jsonRequest string) (jsonReply string, err error) { + ctx := sdk.UnwrapSDKContext(cctx) + var msg registrationAction + if err := json.Unmarshal([]byte(jsonRequest), &msg); err != nil { + return "", err + } + + prefixStore := prefix.NewStore( + ctx.KVStore(k.key), + []byte(watchedAddressStoreKeyPrefix), + ) + switch msg.Type { + case "BRIDGE_TARGET_REGISTER": + prefixStore.Set([]byte(msg.Target), []byte(watchedAddressSentinel)) + case "BRIDGE_TARGET_UNREGISTER": + prefixStore.Delete([]byte(msg.Target)) + default: + return "", sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown action type: %s", msg.Type) + } + return "true", nil +} diff --git a/golang/cosmos/x/vtransfer/module.go b/golang/cosmos/x/vtransfer/module.go new file mode 100644 index 00000000000..2e3088b767f --- /dev/null +++ b/golang/cosmos/x/vtransfer/module.go @@ -0,0 +1,124 @@ +package vtransfer + +import ( + "encoding/json" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/Agoric/agoric-sdk/golang/cosmos/x/vtransfer/types" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/module" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// type check to ensure the interface is properly implemented +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// app module Basics object +type AppModuleBasic struct { +} + +func (AppModuleBasic) Name() string { + return ModuleName +} + +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { +} + +// RegisterInterfaces registers the module's interface types +func (b AppModuleBasic) RegisterInterfaces(registry cdctypes.InterfaceRegistry) { +} + +// DefaultGenesis returns default genesis state as raw bytes for the deployment +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(DefaultGenesisState()) +} + +// Validation check of the Genesis +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, config client.TxEncodingConfig, bz json.RawMessage) error { + var data types.GenesisState + err := cdc.UnmarshalJSON(bz, &data) + if err != nil { + return err + } + // Once json successfully marshalled, passes along to genesis.go + return ValidateGenesis(&data) +} + +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { +} + +// Get the root query command of this module +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// Get the root tx command of this module +func (AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +type AppModule struct { + AppModuleBasic + keeper Keeper +} + +// NewAppModule creates a new AppModule Object +func NewAppModule(k Keeper) AppModule { + am := AppModule{ + AppModuleBasic: AppModuleBasic{}, + keeper: k, + } + return am +} + +func (AppModule) Name() string { + return ModuleName +} + +func (am AppModule) RegisterInvariants(ir sdk.InvariantRegistry) {} + +func (am AppModule) Route() sdk.Route { + return sdk.NewRoute(types.RouterKey, NewHandler(am.keeper)) +} + +func (am AppModule) QuerierRoute() string { + return ModuleName +} + +// LegacyQuerierHandler returns the sdk.Querier for module +func (am AppModule) LegacyQuerierHandler(legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { + return nil +} + +func (am AppModule) RegisterServices(cfg module.Configurator) { +} + +func (AppModule) ConsensusVersion() uint64 { return 1 } + +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { +} + +func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.ValidatorUpdate { + // Prevent Cosmos SDK internal errors. + return []abci.ValidatorUpdate{} +} + +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + var genesisState types.GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + return InitGenesis(ctx, am.keeper, &genesisState) +} + +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := ExportGenesis(ctx, am.keeper) + return cdc.MustMarshalJSON(gs) +} diff --git a/golang/cosmos/x/vtransfer/types/expected_keepers.go b/golang/cosmos/x/vtransfer/types/expected_keepers.go new file mode 100644 index 00000000000..7b40cb37bbf --- /dev/null +++ b/golang/cosmos/x/vtransfer/types/expected_keepers.go @@ -0,0 +1,38 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + capability "github.com/cosmos/cosmos-sdk/x/capability/types" + connection "github.com/cosmos/ibc-go/v6/modules/core/03-connection/types" + channel "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types" + ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported" +) + +// ChannelKeeper defines the expected IBC channel keeper +type ChannelKeeper interface { + GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channel.Channel, found bool) + GetNextSequenceSend(ctx sdk.Context, portID, channelID string) (uint64, bool) + SendPacket(ctx sdk.Context, channelCap *capability.Capability, packet ibcexported.PacketI) error + WriteAcknowledgement(ctx sdk.Context, channelCap *capability.Capability, packet ibcexported.PacketI, acknowledgement ibcexported.Acknowledgement) error + ChanOpenInit(ctx sdk.Context, order channel.Order, connectionHops []string, portID string, + portCap *capability.Capability, counterparty channel.Counterparty, version string) (string, *capability.Capability, error) + WriteOpenInitChannel(ctx sdk.Context, portID, channelID string, order channel.Order, + connectionHops []string, counterparty channel.Counterparty, version string) + ChanCloseInit(ctx sdk.Context, portID, channelID string, chanCap *capability.Capability) error + TimeoutExecuted(ctx sdk.Context, channelCap *capability.Capability, packet ibcexported.PacketI) error +} + +// ClientKeeper defines the expected IBC client keeper +type ClientKeeper interface { + GetClientConsensusState(ctx sdk.Context, clientID string) (connection ibcexported.ConsensusState, found bool) +} + +// ConnectionKeeper defines the expected IBC connection keeper +type ConnectionKeeper interface { + GetConnection(ctx sdk.Context, connectionID string) (connection connection.ConnectionEnd, found bool) +} + +// PortKeeper defines the expected IBC port keeper +type PortKeeper interface { + BindPort(ctx sdk.Context, portID string) *capability.Capability +} diff --git a/golang/cosmos/x/vtransfer/types/genesis.pb.go b/golang/cosmos/x/vtransfer/types/genesis.pb.go new file mode 100644 index 00000000000..69f088dbb80 --- /dev/null +++ b/golang/cosmos/x/vtransfer/types/genesis.pb.go @@ -0,0 +1,327 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: agoric/vtransfer/genesis.proto + +package types + +import ( + fmt "fmt" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// The initial and exported module state. +type GenesisState struct { + WatchedAddresses []github_com_cosmos_cosmos_sdk_types.AccAddress `protobuf:"bytes,1,rep,name=watched_addresses,json=watchedAddresses,proto3,casttype=github.com/cosmos/cosmos-sdk/types.AccAddress" json:"watched_addresses" yaml:"watched_addresses"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_fd0b59a10ad6824e, []int{0} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetWatchedAddresses() []github_com_cosmos_cosmos_sdk_types.AccAddress { + if m != nil { + return m.WatchedAddresses + } + return nil +} + +func init() { + proto.RegisterType((*GenesisState)(nil), "agoric.vtransfer.GenesisState") +} + +func init() { proto.RegisterFile("agoric/vtransfer/genesis.proto", fileDescriptor_fd0b59a10ad6824e) } + +var fileDescriptor_fd0b59a10ad6824e = []byte{ + // 249 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4b, 0x4c, 0xcf, 0x2f, + 0xca, 0x4c, 0xd6, 0x2f, 0x2b, 0x29, 0x4a, 0xcc, 0x2b, 0x4e, 0x4b, 0x2d, 0xd2, 0x4f, 0x4f, 0xcd, + 0x4b, 0x2d, 0xce, 0x2c, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x80, 0xc8, 0xeb, 0xc1, + 0xe5, 0xa5, 0x44, 0xd2, 0xf3, 0xd3, 0xf3, 0xc1, 0x92, 0xfa, 0x20, 0x16, 0x44, 0x9d, 0xd2, 0x32, + 0x46, 0x2e, 0x1e, 0x77, 0x88, 0xce, 0xe0, 0x92, 0xc4, 0x92, 0x54, 0xa1, 0x7e, 0x46, 0x2e, 0xc1, + 0xf2, 0xc4, 0x92, 0xe4, 0x8c, 0xd4, 0x94, 0xf8, 0xc4, 0x94, 0x94, 0xa2, 0xd4, 0xe2, 0xe2, 0xd4, + 0x62, 0x09, 0x46, 0x05, 0x66, 0x0d, 0x1e, 0xa7, 0xa4, 0x57, 0xf7, 0xe4, 0x31, 0x25, 0x3f, 0xdd, + 0x93, 0x97, 0xa8, 0x4c, 0xcc, 0xcd, 0xb1, 0x52, 0xc2, 0x90, 0x52, 0xfa, 0x75, 0x4f, 0x5e, 0x37, + 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x3f, 0x39, 0xbf, 0x38, 0x37, 0xbf, + 0x18, 0x4a, 0xe9, 0x16, 0xa7, 0x64, 0xeb, 0x97, 0x54, 0x16, 0xa4, 0x16, 0xeb, 0x39, 0x26, 0x27, + 0x3b, 0x42, 0xf4, 0x04, 0x09, 0x40, 0x0d, 0x71, 0x84, 0x99, 0x61, 0xc5, 0xf2, 0x62, 0x81, 0x3c, + 0x83, 0x53, 0xd8, 0x89, 0x47, 0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x38, + 0xe1, 0xb1, 0x1c, 0xc3, 0x85, 0xc7, 0x72, 0x0c, 0x37, 0x1e, 0xcb, 0x31, 0x44, 0xd9, 0x20, 0x59, + 0xe0, 0x08, 0x09, 0x15, 0x88, 0xe7, 0xc1, 0x16, 0xa4, 0xe7, 0xe7, 0x24, 0xe6, 0xa5, 0xc3, 0x6c, + 0xae, 0x40, 0x0a, 0x30, 0xb0, 0xd5, 0x49, 0x6c, 0xe0, 0x70, 0x30, 0x06, 0x04, 0x00, 0x00, 0xff, + 0xff, 0x78, 0x52, 0xc9, 0xd5, 0x51, 0x01, 0x00, 0x00, +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.WatchedAddresses) > 0 { + for iNdEx := len(m.WatchedAddresses) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.WatchedAddresses[iNdEx]) + copy(dAtA[i:], m.WatchedAddresses[iNdEx]) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.WatchedAddresses[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.WatchedAddresses) > 0 { + for _, b := range m.WatchedAddresses { + l = len(b) + n += 1 + l + sovGenesis(uint64(l)) + } + } + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field WatchedAddresses", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.WatchedAddresses = append(m.WatchedAddresses, make([]byte, postIndex-iNdEx)) + copy(m.WatchedAddresses[len(m.WatchedAddresses)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/golang/cosmos/x/vtransfer/types/key.go b/golang/cosmos/x/vtransfer/types/key.go new file mode 100644 index 00000000000..15963488def --- /dev/null +++ b/golang/cosmos/x/vtransfer/types/key.go @@ -0,0 +1,9 @@ +package types + +const ( + // module name + ModuleName = "vtransfer" + + // StoreKey to be used when creating the KVStore + StoreKey = ModuleName +) diff --git a/golang/cosmos/x/vtransfer/types/msgs.go b/golang/cosmos/x/vtransfer/types/msgs.go new file mode 100644 index 00000000000..3708dbb59e4 --- /dev/null +++ b/golang/cosmos/x/vtransfer/types/msgs.go @@ -0,0 +1,9 @@ +package types + +const RouterKey = ModuleName // this was defined in your key.go file + +type InvokeMemo struct { + InvokeOnAcknowledgementPacket string `json:"invokeOnAcknowledgementPacket"` + InvokeOnTimeoutPacket string `json:"invokeOnTimeoutPacket"` + InvokeWriteAcknowledgement string `json:"invokeWriteAcknowledgement"` +} diff --git a/packages/boot/test/boot-config.test.js b/packages/boot/test/boot-config.test.js index edbd8113181..1c324abe770 100644 --- a/packages/boot/test/boot-config.test.js +++ b/packages/boot/test/boot-config.test.js @@ -34,7 +34,7 @@ const CONFIG_FILES = [ ...PROD_CONFIG_FILES, ]; -const NON_UPGRADEABLE_VATS = ['vat-network', 'vat-ibc', 'pegasus', 'mints']; +const NON_UPGRADEABLE_VATS = ['pegasus', 'mints']; /** * @param {string} bin diff --git a/packages/boot/test/bootstrapTests/demo-config.test.ts b/packages/boot/test/bootstrapTests/demo-config.test.ts index bf84f81e6fa..3098a7d77e2 100644 --- a/packages/boot/test/bootstrapTests/demo-config.test.ts +++ b/packages/boot/test/bootstrapTests/demo-config.test.ts @@ -3,9 +3,8 @@ import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { PowerFlags } from '@agoric/vats/src/walletFlags.js'; import type { TestFn } from 'ava'; -import type { NameAdmin, NameHub } from '@agoric/vats'; -import { makeSwingsetTestKit, keyArrayEqual } from '../../tools/supports.ts'; +import { keyArrayEqual, makeSwingsetTestKit } from '../../tools/supports.ts'; const { keys } = Object; @@ -76,6 +75,7 @@ test('namesByAddress contains provisioned account', async t => { const { EV } = t.context.runUtils; const addr = 'agoric1234new'; const home = await makeHomeFor(addr, EV); + t.truthy(home); const namesByAddress = await EV.vat('bootstrap').consumeItem('namesByAddress'); await t.notThrowsAsync(EV(namesByAddress).lookup(addr)); diff --git a/packages/boot/test/bootstrapTests/net-ibc-upgrade.test.ts b/packages/boot/test/bootstrapTests/net-ibc-upgrade.test.ts index 47dcc7d963c..868dd4bf5fa 100644 --- a/packages/boot/test/bootstrapTests/net-ibc-upgrade.test.ts +++ b/packages/boot/test/bootstrapTests/net-ibc-upgrade.test.ts @@ -31,7 +31,7 @@ export const makeTestContext = async t => { }) as Baggage; const zone = makeDurableZone(baggage); - const bundleDir = 'bundles/vaults'; + const bundleDir = 'bundles/net-ibc-upgrade'; const bundleCache = await makeNodeBundleCache( bundleDir, { cacheSourceMaps: false }, @@ -61,27 +61,9 @@ test.serial('bootstrap produces provisioning vat', async t => { t.truthy(provisioning); }); -test.serial('network vat core eval launches network, ibc vats', async t => { - const { controller, buildProposal } = t.context; +test.serial('bootstrap launches network, ibc vats', async t => { const { EV } = t.context.runUtils; - t.log('building network proposal'); - const proposal = await buildProposal( - '@agoric/builders/scripts/vats/init-network.js', - ); - - for await (const bundle of proposal.bundles) { - await controller.validateAndInstallBundle(bundle); - } - t.log('installed', proposal.bundles.length, 'bundles'); - - t.log('executing', proposal.evals.length, 'core evals'); - const bridgeMessage = { type: 'CORE_EVAL', evals: proposal.evals }; - const coreHandler: BridgeHandler = await EV.vat('bootstrap').consumeItem( - 'coreEvalBridgeHandler', - ); - await EV(coreHandler).fromBridge(bridgeMessage); - t.log('network proposal executed'); const vatStore = await EV.vat('bootstrap').consumeItem('vatStore'); t.true(await EV(vatStore).has('ibc'), 'ibc'); @@ -106,8 +88,10 @@ const upgradeVats = async (t, EV, vatsToUpgrade) => { await EV.vat('bootstrap').consumeItem('vatUpgradeInfo'); const vatAdminSvc = await EV.vat('bootstrap').consumeItem('vatAdminSvc'); for (const vatName of vatsToUpgrade) { - const { bundleID } = await EV(vatUpgradeInfo).get(vatName); - const bcap = await EV(vatAdminSvc).getBundleCap(bundleID); + const { bundleID, bundleName } = await EV(vatUpgradeInfo).get(vatName); + const bcap = await (bundleID + ? EV(vatAdminSvc).getBundleCap(bundleID) + : EV(vatAdminSvc).getNamedBundleCap(bundleName)); const { adminNode } = await EV(vatStore).get(vatName); const result = await EV(adminNode).upgrade(bcap); t.log(vatName, result); diff --git a/packages/boot/test/bootstrapTests/orchestration.test.ts b/packages/boot/test/bootstrapTests/orchestration.test.ts index ddc95ce7c2f..e6f5b9d1da1 100644 --- a/packages/boot/test/bootstrapTests/orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/orchestration.test.ts @@ -23,10 +23,7 @@ test.serial('stakeAtom - repl-style', async t => { evalProposal, runUtils: { EV }, } = t.context; - // TODO move into a vm-config - await evalProposal( - buildProposal('@agoric/builders/scripts/vats/init-network.js'), - ); + // TODO move into a vm-config for future agoric-upgrade await evalProposal( buildProposal('@agoric/builders/scripts/vats/init-orchestration.js'), ); diff --git a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts index 74c97b2e9dc..f0172ca4c2a 100644 --- a/packages/boot/test/bootstrapTests/vat-orchestration.test.ts +++ b/packages/boot/test/bootstrapTests/vat-orchestration.test.ts @@ -55,10 +55,7 @@ test.before(async t => { evalProposal, runUtils: { EV }, } = t.context; - /** ensure network, ibc, and orchestration are available */ - await evalProposal( - buildProposal('@agoric/builders/scripts/vats/init-network.js'), - ); + /** ensure orchestration is available */ await evalProposal( buildProposal('@agoric/builders/scripts/vats/init-orchestration.js'), ); diff --git a/packages/boot/test/bootstrapTests/vats-restart.test.ts b/packages/boot/test/bootstrapTests/vats-restart.test.ts index fd3c35ef96f..b23c5f61ee0 100644 --- a/packages/boot/test/bootstrapTests/vats-restart.test.ts +++ b/packages/boot/test/bootstrapTests/vats-restart.test.ts @@ -23,9 +23,13 @@ const PLATFORM_CONFIG = '@agoric/vm-config/decentral-itest-vaults-config.json'; export const makeTestContext = async t => { console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/vaults', { - configSpecifier: PLATFORM_CONFIG, - }); + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/vats-restart', + { + configSpecifier: PLATFORM_CONFIG, + }, + ); const { runUtils, storage } = swingsetTestKit; console.timeLog('DefaultTestContext', 'swingsetTestKit'); @@ -94,17 +98,6 @@ test.serial('open vault', async t => { }); }); -test.serial('run network vat proposal', async t => { - const { buildProposal, evalProposal } = t.context; - - t.log('building network proposal'); - await evalProposal( - buildProposal('@agoric/builders/scripts/vats/init-network.js'), - ); - - t.pass(); // reached here without throws -}); - test.serial('make IBC callbacks before upgrade', async t => { const { EV } = t.context.runUtils; const vatStore = await EV.vat('bootstrap').consumeItem('vatStore'); diff --git a/packages/boot/test/bootstrapTests/vaults-integration.test.ts b/packages/boot/test/bootstrapTests/vaults-integration.test.ts index 9d66c9a6f3b..b6f24417e36 100644 --- a/packages/boot/test/bootstrapTests/vaults-integration.test.ts +++ b/packages/boot/test/bootstrapTests/vaults-integration.test.ts @@ -31,7 +31,10 @@ const likePayouts = (collateral, minted) => ({ const makeDefaultTestContext = async t => { console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/vaults'); + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/vaults-integration', + ); const { runUtils, storage } = swingsetTestKit; console.timeLog('DefaultTestContext', 'swingsetTestKit'); diff --git a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts index cb7cf1233b7..1013619428c 100644 --- a/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts +++ b/packages/boot/test/bootstrapTests/vaults-upgrade.test.ts @@ -30,9 +30,13 @@ const makeDefaultTestContext = async ( } = {}, ) => { logTiming && console.time('DefaultTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/vaults', { - storage, - }); + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/vaults-upgrade', + { + storage, + }, + ); const { readLatest, runUtils } = swingsetTestKit; ({ storage } = swingsetTestKit); diff --git a/packages/boot/test/bootstrapTests/vtransfer.test.ts b/packages/boot/test/bootstrapTests/vtransfer.test.ts new file mode 100644 index 00000000000..f486df7d1f0 --- /dev/null +++ b/packages/boot/test/bootstrapTests/vtransfer.test.ts @@ -0,0 +1,113 @@ +/* eslint-disable @jessie.js/safe-await-separator -- confused by casting 'as' */ +import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js'; + +import type { TestFn } from 'ava'; + +import type { ScopedBridgeManager } from '@agoric/vats'; +import type { TransferMiddleware } from '@agoric/vats/src/transfer.js'; +import type { TransferVat } from '@agoric/vats/src/vat-transfer.js'; +import { BridgeId, VTRANSFER_IBC_EVENT } from '@agoric/internal'; +import { makeSwingsetTestKit } from '../../tools/supports.ts'; + +const makeDefaultTestContext = async t => { + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/vtransfer', + { configSpecifier: '@agoric/vm-config/decentral-demo-config.json' }, + ); + return swingsetTestKit; +}; + +type DefaultTestContext = Awaited>; + +const test: TestFn = anyTest; + +test.before(async t => (t.context = await makeDefaultTestContext(t))); +test.after.always(t => t.context.shutdown?.()); + +test('vtransfer', async t => { + const { buildProposal, evalProposal, getOutboundMessages, runUtils } = + t.context; + const { EV } = runUtils; + + // Pull what transfer-proposal produced into local scope + const transferVat = (await EV.vat('bootstrap').consumeItem( + 'transferVat', + )) as ERef; + t.truthy(transferVat); + const transferMiddleware = (await EV.vat('bootstrap').consumeItem( + 'transferMiddleware', + )) as TransferMiddleware; + t.truthy(transferMiddleware); + const vtransferBridgeManager = (await EV.vat('bootstrap').consumeItem( + 'vtransferBridgeManager', + )) as ScopedBridgeManager<'vtransfer'>; + t.truthy(vtransferBridgeManager); + + // only VTRANSFER_IBC_EVENT is supported by vtransferBridgeManager + await t.throwsAsync( + EV(vtransferBridgeManager).fromBridge({ + type: 'VTRANSFER_OTHER', + }), + { + message: `Invalid inbound event type "VTRANSFER_OTHER"; expected "${VTRANSFER_IBC_EVENT}"`, + }, + ); + + const target = 'agoric1vtransfertest'; + const packet = 'thisIsPacket'; + + // 0 interceptors for target + + // it's an error to target an address before an interceptor is registered + await t.throwsAsync( + EV(vtransferBridgeManager).fromBridge({ + target, + type: VTRANSFER_IBC_EVENT, + event: 'echo', + }), + { + message: + 'key "agoric1vtransfertest" not found in collection "targetToApp"', + }, + ); + + // 1 interceptors for target + + // Tap into VTRANSFER_IBC_EVENT messages + const testVtransferProposal = buildProposal( + '@agoric/builders/scripts/vats/test-vtransfer.js', + ); + await evalProposal(testVtransferProposal); + + // simulate a Golang upcall with arbitrary payload + // note that property order matters! + const expectedAckData = { + event: 'writeAcknowledgement', + packet, + target, + type: VTRANSFER_IBC_EVENT, + }; + + await EV(vtransferBridgeManager).fromBridge(expectedAckData); + + // verify the ackMethod outbound + const messages = getOutboundMessages(BridgeId.VTRANSFER); + t.deepEqual(messages, [ + { + target, + type: 'BRIDGE_TARGET_REGISTER', + }, + { + ack: btoa(JSON.stringify(expectedAckData)), + method: 'receiveExecuted', + packet, + type: 'IBC_METHOD', + }, + ]); + + // test adding an interceptor for the same target, which should fail + await t.throwsAsync(() => evalProposal(testVtransferProposal), { + message: /Target.*already registered/, + }); +}); diff --git a/packages/boot/test/bootstrapTests/walletFactory.ts b/packages/boot/test/bootstrapTests/walletFactory.ts index 515cb75df3b..2f844ec8de7 100644 --- a/packages/boot/test/bootstrapTests/walletFactory.ts +++ b/packages/boot/test/bootstrapTests/walletFactory.ts @@ -8,9 +8,13 @@ import { makeWalletFactoryDriver } from '../../tools/drivers.ts'; const { Fail } = assert; export const makeWalletFactoryContext = async t => { - const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/vaults', { - configSpecifier: '@agoric/vm-config/decentral-main-vaults-config.json', - }); + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/walletFactory', + { + configSpecifier: '@agoric/vm-config/decentral-main-vaults-config.json', + }, + ); const { runUtils, storage } = swingsetTestKit; console.timeLog('DefaultTestContext', 'swingsetTestKit'); diff --git a/packages/boot/test/bootstrapTests/zcf-upgrade.test.ts b/packages/boot/test/bootstrapTests/zcf-upgrade.test.ts index 4fcda87ffbf..949957330aa 100644 --- a/packages/boot/test/bootstrapTests/zcf-upgrade.test.ts +++ b/packages/boot/test/bootstrapTests/zcf-upgrade.test.ts @@ -35,9 +35,13 @@ const ZCF_PROBE_SRC = './zcfProbe.js'; export const makeZoeTestContext = async t => { console.time('ZoeTestContext'); - const swingsetTestKit = await makeSwingsetTestKit(t.log, 'bundles/zoe', { - configSpecifier: '@agoric/vm-config/decentral-demo-config.json', - }); + const swingsetTestKit = await makeSwingsetTestKit( + t.log, + 'bundles/zcf-upgrade', + { + configSpecifier: '@agoric/vm-config/decentral-demo-config.json', + }, + ); const { runUtils } = swingsetTestKit; console.timeLog('DefaultTestContext', 'swingsetTestKit'); diff --git a/packages/boot/tools/supports.ts b/packages/boot/tools/supports.ts index ee211d1c89a..f5ba1c82b6a 100644 --- a/packages/boot/tools/supports.ts +++ b/packages/boot/tools/supports.ts @@ -22,10 +22,10 @@ import { boardSlottingMarshaller, slotToBoardRemote, } from '@agoric/vats/tools/board-utils.js'; +import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; import type { ExecutionContext as AvaT } from 'ava'; -import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js'; import type { CoreEvalSDKType } from '@agoric/cosmic-proto/swingset/swingset.js'; import type { BridgeHandler, IBCMethod } from '@agoric/vats'; import { icaMocks, protoMsgMocks } from './ibc/mocks.js'; diff --git a/packages/builders/scripts/vats/init-transfer.js b/packages/builders/scripts/vats/init-transfer.js new file mode 100644 index 00000000000..5437e768ccb --- /dev/null +++ b/packages/builders/scripts/vats/init-transfer.js @@ -0,0 +1,18 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async ({ publishRef, install }) => + harden({ + sourceSpec: '@agoric/vats/src/proposals/transfer-proposal.js', + getManifestCall: [ + 'getManifestForTransfer', + { + transferRef: publishRef(install('@agoric/vats/src/vat-transfer.js')), + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('gov-transfer', defaultProposalBuilder); +}; diff --git a/packages/builders/scripts/vats/test-vtransfer.js b/packages/builders/scripts/vats/test-vtransfer.js new file mode 100644 index 00000000000..f55fa5ad5e3 --- /dev/null +++ b/packages/builders/scripts/vats/test-vtransfer.js @@ -0,0 +1,18 @@ +import { makeHelpers } from '@agoric/deploy-script-support'; + +/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */ +export const defaultProposalBuilder = async _powers => + harden({ + sourceSpec: '@agoric/vats/src/proposals/vtransfer-echoer.js', + getManifestCall: [ + 'getManifestForVtransferEchoer', + { + target: 'agoric1vtransfertest', + }, + ], + }); + +export default async (homeP, endowments) => { + const { writeCoreProposal } = await makeHelpers(homeP, endowments); + await writeCoreProposal('test-vtransfer', defaultProposalBuilder); +}; diff --git a/packages/cosmic-swingset/economy-template.json b/packages/cosmic-swingset/economy-template.json index 0e0f9b2a004..3964d202326 100644 --- a/packages/cosmic-swingset/economy-template.json +++ b/packages/cosmic-swingset/economy-template.json @@ -5,7 +5,8 @@ ], [ "@agoric/builders/scripts/vats/init-network.js", - "@agoric/builders/scripts/pegasus/init-core.js" + "@agoric/builders/scripts/pegasus/init-core.js", + "@agoric/builders/scripts/vats/init-transfer.js" ], [ { diff --git a/packages/deployment/Makefile b/packages/deployment/Makefile index 59d3c6de3a2..bfacaa32209 100644 --- a/packages/deployment/Makefile +++ b/packages/deployment/Makefile @@ -1,6 +1,7 @@ REPOSITORY = ghcr.io/agoric/cosmic-swingset REPOSITORY_SDK = ghcr.io/agoric/agoric-sdk SS := ../cosmic-swingset/ +DOCKER_BUILD ?= docker build TAG := unreleased @@ -8,29 +9,29 @@ default: docker-build docker-show-fat: date > show-fat-bust-cache.stamp - docker build --file=Dockerfile.show-fat ../.. + $(DOCKER_BUILD) --file=Dockerfile.show-fat ../.. docker-build: docker-build-sdk docker-build-solo \ docker-build-setup docker-build-ssh-node docker-build-sdk: bargs=`node ../xsnap/src/build.js --show-env | sed -e 's/^/ --build-arg=/'`; \ - docker build $$bargs \ + $(DOCKER_BUILD) $$bargs \ -t $(REPOSITORY_SDK):$(TAG) --file=Dockerfile.sdk ../.. docker tag $(REPOSITORY_SDK):$(TAG) $(REPOSITORY_SDK):latest docker-build-setup: - docker build --build-arg=TAG=$(TAG) -t $(REPOSITORY)-setup:$(TAG) . + $(DOCKER_BUILD) --build-arg=TAG=$(TAG) -t $(REPOSITORY)-setup:$(TAG) . docker tag $(REPOSITORY)-setup:$(TAG) $(REPOSITORY)-setup:latest docker-build-solo: - docker build --build-arg=TAG=$(TAG) -t $(REPOSITORY)-solo:$(TAG) ../solo + $(DOCKER_BUILD) --build-arg=TAG=$(TAG) -t $(REPOSITORY)-solo:$(TAG) ../solo docker tag $(REPOSITORY)-solo:$(TAG) $(REPOSITORY)-solo:latest docker-build-ssh-node: - docker build --build-arg=TAG=$(TAG) -t ghcr.io/agoric/ssh-node:$(TAG) --file=Dockerfile.ssh-node ./docker + $(DOCKER_BUILD) --build-arg=TAG=$(TAG) -t ghcr.io/agoric/ssh-node:$(TAG) --file=Dockerfile.ssh-node ./docker docker tag ghcr.io/agoric/ssh-node:$(TAG) ghcr.io/agoric/ssh-node:latest # ./docker is an emptyish directory. docker-build-ibc-alpha: - docker build --build-arg=SDK_TAG=$(TAG) -t $(REPOSITORY_SDK):ibc-alpha --file=Dockerfile.ibc-alpha ./docker + $(DOCKER_BUILD) --build-arg=SDK_TAG=$(TAG) -t $(REPOSITORY_SDK):ibc-alpha --file=Dockerfile.ibc-alpha ./docker diff --git a/packages/internal/src/config.js b/packages/internal/src/config.js index 06e146e9741..5e9ce91c9bd 100644 --- a/packages/internal/src/config.js +++ b/packages/internal/src/config.js @@ -30,18 +30,20 @@ export const BridgeId = /** @type {const} */ ({ harden(BridgeId); /** @typedef {(typeof BridgeId)[keyof typeof BridgeId]} BridgeIdValue */ +export const VTRANSFER_IBC_EVENT = 'VTRANSFER_IBC_EVENT'; + export const CosmosInitKeyToBridgeId = { vbankPort: BridgeId.BANK, vibcPort: BridgeId.DIBC, }; -export const WalletName = { +export const WalletName = /** @type {const} */ ({ depositFacet: 'depositFacet', -}; +}); harden(WalletName); // defined in golang/cosmos/x/vbank -export const VBankAccount = { +export const VBankAccount = /** @type {const} */ ({ reserve: { module: 'vbank/reserve', address: 'agoric1ae0lmtzlgrcnla9xjkpaarq5d5dfez63h3nucl', @@ -50,5 +52,5 @@ export const VBankAccount = { module: 'vbank/provision', address: 'agoric1megzytg65cyrgzs6fvzxgrcqvwwl7ugpt62346', }, -}; +}); harden(VBankAccount); diff --git a/packages/network/package.json b/packages/network/package.json index 7ba9f082f1b..6fdc9586e55 100644 --- a/packages/network/package.json +++ b/packages/network/package.json @@ -27,6 +27,7 @@ "@agoric/vat-data": "^0.5.2", "@endo/base64": "^1.0.5", "@endo/far": "^1.1.2", + "@endo/pass-style": "^1.4.0", "@endo/patterns": "^1.4.0", "@endo/promise-kit": "^1.1.2" }, diff --git a/packages/network/src/bytes.js b/packages/network/src/bytes.js index 547d0dd5cd8..e952b1b0dc4 100644 --- a/packages/network/src/bytes.js +++ b/packages/network/src/bytes.js @@ -1,7 +1,7 @@ // @ts-check -/// -import { Fail } from '@agoric/assert'; +import { details, Fail } from '@agoric/assert'; import { encodeBase64, decodeBase64 } from '@endo/base64'; +import { isObject } from '@endo/pass-style'; /** * @import {Bytes} from './types.js'; @@ -9,6 +9,33 @@ import { encodeBase64, decodeBase64 } from '@endo/base64'; /** @typedef {Bytes | Buffer | Uint8Array | Iterable} ByteSource */ +/** + * This function is a coercer instead of an asserter because in a future where + * binary data has better support across vats and potentially its own type, we + * might allow more `specimen`s than just `ByteSource`. + * + * @param {unknown} specimen + * @returns {ByteSource} + */ +export function coerceToByteSource(specimen) { + if (typeof specimen === 'string') { + return specimen; + } + + isObject(specimen) || + assert.fail(details`non-object ${specimen} is not a ByteSource`, TypeError); + + const obj = /** @type {{}} */ (specimen); + typeof obj[Symbol.iterator] === 'function' || + assert.fail( + details`non-iterable ${specimen} is not a ByteSource`, + TypeError, + ); + + // Good enough... it's iterable and can be converted later. + return /** @type {ByteSource} */ (specimen); +} + /** * @param {ByteSource} contents */ @@ -60,7 +87,7 @@ export function bytesToString(bytes) { * @param {ByteSource} byteSource * @returns {string} base64 encoding */ -export function dataToBase64(byteSource) { +export function byteSourceToBase64(byteSource) { const bytes = coerceToByteArray(byteSource); return encodeBase64(bytes); } diff --git a/packages/network/src/network.js b/packages/network/src/network.js index 32458ca0a75..156c8dd34a4 100644 --- a/packages/network/src/network.js +++ b/packages/network/src/network.js @@ -584,9 +584,7 @@ const preparePort = (zone, powers) => { // Clean up everything we did. const values = [...currentConnections.get(port).values()]; - /** @type {import('@agoric/vow').Specimen[]} */ const ps = []; - ps.push( ...values.map(conn => watch(E(conn).close(), this.facets.sinkWatcher), diff --git a/packages/orchestration/test/exos/local-chain-account-kit.test.ts b/packages/orchestration/test/exos/local-chain-account-kit.test.ts index 46b1a6c0e8e..06e6b4469a3 100644 --- a/packages/orchestration/test/exos/local-chain-account-kit.test.ts +++ b/packages/orchestration/test/exos/local-chain-account-kit.test.ts @@ -3,10 +3,10 @@ import { test } from '@agoric/zoe/tools/prepare-test-env-ava.js'; import { AmountMath } from '@agoric/ertp'; import { prepareRecorderKitMakers } from '@agoric/zoe/src/contractSupport/recorder.js'; import { E, Far } from '@endo/far'; +import { commonSetup } from '../supports.js'; import { prepareLocalChainAccountKit } from '../../src/exos/local-chain-account-kit.js'; import { ChainAddress } from '../../src/orchestration-api.js'; import { NANOSECONDS_PER_SECOND } from '../../src/utils/time.js'; -import { commonSetup } from '../supports.js'; import { wellKnownChainInfo } from '../../src/chain-info.js'; const agoricChainInfo = wellKnownChainInfo.agoric; diff --git a/packages/orchestration/test/supports.ts b/packages/orchestration/test/supports.ts index 0989d18f096..cf666748556 100644 --- a/packages/orchestration/test/supports.ts +++ b/packages/orchestration/test/supports.ts @@ -1,9 +1,15 @@ import { makeIssuerKit } from '@agoric/ertp'; +import { VTRANSFER_IBC_EVENT } from '@agoric/internal'; import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; import { prepareLocalChainTools } from '@agoric/vats/src/localchain.js'; +import { prepareTransferTools } from '@agoric/vats/src/transfer.js'; import { makeFakeBankManagerKit } from '@agoric/vats/tools/bank-utils.js'; import { makeFakeBoard } from '@agoric/vats/tools/board-utils.js'; -import { makeFakeLocalchainBridge } from '@agoric/vats/tools/fake-bridge.js'; +import { + makeFakeLocalchainBridge, + makeFakeTransferBridge, +} from '@agoric/vats/tools/fake-bridge.js'; +import { prepareVowTools } from '@agoric/vow'; import type { Installation } from '@agoric/zoe/src/zoeService/utils.js'; import { buildZoeManualTimer } from '@agoric/zoe/tools/manualTimer.js'; import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; @@ -20,7 +26,10 @@ import { wellKnownChainInfo, } from '../src/chain-info.js'; -export { makeFakeLocalchainBridge } from '@agoric/vats/tools/fake-bridge.js'; +export { + makeFakeLocalchainBridge, + makeFakeTransferBridge, +} from '@agoric/vats/tools/fake-bridge.js'; export const commonSetup = async t => { t.log('bootstrap vat dependencies'); @@ -41,12 +50,28 @@ export const commonSetup = async t => { ist.issuerKit, ); + const transferBridge = makeFakeTransferBridge(rootZone); + const { makeTransferMiddlewareKit, makeBridgeTargetKit } = + prepareTransferTools( + rootZone.subZone('transfer'), + prepareVowTools(rootZone.subZone('vows')), + ); + const { finisher, interceptorFactory, transferMiddleware } = + makeTransferMiddlewareKit(); + const bridgeTargetKit = makeBridgeTargetKit( + transferBridge, + VTRANSFER_IBC_EVENT, + interceptorFactory, + ); + finisher.useRegistry(bridgeTargetKit.targetRegistry); + const localchainBridge = makeFakeLocalchainBridge(rootZone); const localchain = prepareLocalChainTools( rootZone.subZone('localchain'), ).makeLocalChain({ bankManager, system: localchainBridge, + transfer: transferMiddleware, }); const timer = buildZoeManualTimer(t.log); const marshaller = makeFakeBoard().getReadonlyMarshaller(); diff --git a/packages/vats/package.json b/packages/vats/package.json index e61a97130f9..5510a296a5a 100644 --- a/packages/vats/package.json +++ b/packages/vats/package.json @@ -36,7 +36,6 @@ "@agoric/vow": "^0.1.0", "@agoric/zoe": "^0.26.2", "@agoric/zone": "^0.2.2", - "@endo/base64": "^1.0.5", "@endo/far": "^1.1.2", "@endo/import-bundle": "^1.1.2", "@endo/marshal": "^1.5.0", diff --git a/packages/vats/src/bridge-target.js b/packages/vats/src/bridge-target.js new file mode 100644 index 00000000000..e5ea1d29228 --- /dev/null +++ b/packages/vats/src/bridge-target.js @@ -0,0 +1,316 @@ +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; + +import { BridgeHandlerI } from './bridge.js'; + +/** + * @typedef {any} MostlyPureData ideally should be PureData, but that type is + * too restrictive to work out-of-the-box. + */ + +const { details: X, Fail } = assert; + +/** + * @typedef {object} TargetApp an object representing the app that receives + * upcalls from the low-level TargetHost on the other side of a bridge + * @property {(obj: MostlyPureData) => Promise} receiveUpcall + * receive data from the TargetHost, and return a data result + */ +// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163 +export const TargetAppI = M.interface('TargetApp', { + receiveUpcall: M.callWhen(M.any()).returns(M.any()), +}); + +/** + * @typedef {object} TargetHost an object representing the host that receives + * downcalls from the high-level TargetApp over a bridge + * @property {(obj: MostlyPureData) => Promise} sendDowncall + * send data to the TargetHost, which returns a data result + */ +// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163 +export const TargetHostI = M.interface('TargetHost', { + sendDowncall: M.callWhen(M.any()).returns(M.any()), +}); + +/** + * @typedef {object} AppTransformer an object for replacing a TargetApp, + * generally with a wrapper that intercepts its inputs and/or results to + * ensure conformance with a protocol and/or interface (e.g., an active tap + * can encode results of the wrapped TargetApp into an acknowledgement + * message) + * @property {( + * app: ERef, + * targetHost: ERef, + * ...args: unknown[] + * ) => ERef} wrapApp + */ +// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163 +export const AppTransformerI = M.interface('AppTransformer', { + wrapApp: M.callWhen( + M.await(M.remotable('TargetAppI')), + M.await(M.remotable('TargetHostI')), + ) + .rest(M.any()) + .returns(M.remotable('TargetAppI')), +}); + +/** + * @typedef {object} TargetRegistration is an ExoClass of its own, and each + * instance is an attenuation of a BridgeTargetKit `targetRegistry` facet that + * has access to internal state thereof but has been scoped down to a single + * target. + * @property {() => Promise} revoke performs unregistration (and sends the + * corresponding "BRIDGE_TARGET_UNREGISTER" message to the targetHost). + * @property {(targetApp: ERef) => Promise} updateTargetApp + * replaces the app associated with the target (but with no corresponding + * message). + */ +// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163 +export const TargetRegistrationI = M.interface('TargetRegistration', { + updateTargetApp: M.callWhen(M.await(M.remotable('TargetAppI'))).returns(), + revoke: M.callWhen().returns(), +}); + +/** + * @typedef {object} TargetRegistry + * @property {( + * target: string, + * targetApp: ERef, + * args?: unknown[], + * ) => Promise} register + * @property {( + * target: string, + * targetApp: ERef, + * args?: unknown[], + * ) => Promise} reregister + * @property {(target: string) => Promise} unregister + */ +// TODO unwrap type https://github.com/Agoric/agoric-sdk/issues/9163 +const TargetRegistryI = M.interface('TargetRegistry', { + register: M.callWhen(M.string(), M.await(M.remotable('TargetAppI'))) + .optional(M.array()) + .returns(M.remotable('TargetRegistration')), + reregister: M.callWhen(M.string(), M.await(M.remotable('TargetAppI'))) + .optional(M.array()) + .returns(), + unregister: M.callWhen(M.string()).returns(), +}); + +/** @param {import('@agoric/base-zone').Zone} zone */ +export const prepareTargetRegistration = zone => + zone.exoClass( + 'TargetRegistration', + TargetRegistrationI, + /** + * @param {string} target + * @param {TargetRegistry} registry + * @param {unknown[] | null} args + */ + (target, registry, args) => ({ + target, + /** @type {TargetRegistry | null} */ + registry, + args, + }), + { + /** + * Atomically point the registration at a different app. + * + * @param {TargetApp} app new app to handle messages for the target + */ + async updateTargetApp(app) { + const { target, registry, args } = this.state; + if (!registry) { + throw Fail`Registration for ${target} is already revoked`; + } + return E(registry).reregister( + target, + app, + /** @type {unknown[]} */ (args), + ); + }, + /** Atomically delete the registration. */ + async revoke() { + const { target, registry } = this.state; + if (!registry) { + throw Fail`Registration for ${target} is already revoked`; + } + this.state.registry = null; + this.state.args = null; + return E(registry).unregister(target); + }, + }, + ); + +/** + * A BridgeTargetKit is associated with a ScopedBridgeManager (essentially a + * specific named channel on a bridge, cf. {@link ./bridge.js}) and a particular + * inbound event type and an optional app transformer. It consists of three + * facets: + * + * - `targetHost` has a `downcall` method for sending outbound messages via the + * bridge to the VM host. + * - `targetRegistry` has `register`, `reregister` and `unregister` methods to + * register/reregister/unregister an "app" (or rather its transformation) with + * a "target" corresponding to an address on the targetHost. Each target may + * be associated with at most one app at any given time, and registration and + * unregistration each send a message to the targetHost of the state change + * (of type "BRIDGE_TARGET_REGISTER" and "BRIDGE_TARGET_UNREGISTER", + * respectively). `reregister` is a method that atomically redirects the + * target to a new (transformed) app. + * - `bridgeHandler` has a `fromBridge` method for receiving from the + * ScopedBridgeManager inbound messages of the associated event type and + * dispatching them to the app registered for their target. + * + * @param {import('@agoric/base-zone').Zone} zone + * @param {ReturnType} makeTargetRegistration + */ +export const prepareBridgeTargetKit = (zone, makeTargetRegistration) => + zone.exoClassKit( + 'BridgeTargetKit', + { + bridgeHandler: BridgeHandlerI, + targetHost: TargetHostI, + targetRegistry: TargetRegistryI, + }, + /** + * @template {import('@agoric/internal').BridgeIdValue} T + * @param {import('./types').ScopedBridgeManager} manager + * @param {string} inboundEventType + * @param {AppTransformer} [appTransformer] + */ + (manager, inboundEventType, appTransformer = undefined) => ({ + manager, + inboundEventType, + appTransformer, + /** @type {MapStore>} */ + targetToApp: zone.detached().mapStore('targetToApp'), + }), + { + bridgeHandler: { + fromBridge(obj) { + const { inboundEventType, targetToApp } = this.state; + const { type, target } = obj; + + type === inboundEventType || + Fail`Invalid inbound event type ${type}; expected ${inboundEventType}`; + + target || Fail`Missing target property in ${obj}`; + + const app = targetToApp.get(target); + return E(app).receiveUpcall(obj); + }, + }, + targetHost: { + async sendDowncall(obj) { + const { manager } = this.state; + return E(manager).toBridge(obj); + }, + }, + targetRegistry: { + /** + * Register an app to handle messages for a target. + * + * @param {string} target + * @param {TargetApp} app + * @param {unknown[]} [args] + * @returns {Promise} power to set or delete the + * registration + */ + async register(target, app, args = []) { + const { targetHost } = this.facets; + const { appTransformer, targetToApp } = this.state; + + // Because wrapping an app is async, we verify absence of an existing + // registration twice (once to avoid the unnecessary invocation and + // once inside `init`), but attempt to throw similar errors in both + // cases. + !targetToApp.has(target) || Fail`Target ${target} already registered`; + const wrappedApp = await (appTransformer + ? E(appTransformer).wrapApp(app, targetHost, ...args) + : app); + try { + targetToApp.init(target, wrappedApp); + } catch (cause) { + throw assert.error( + X`Target ${target} already registered`, + undefined, + { cause }, + ); + } + + await E(targetHost).sendDowncall({ + type: 'BRIDGE_TARGET_REGISTER', + target, + }); + + return makeTargetRegistration( + target, + this.facets.targetRegistry, + args, + ); + }, + /** + * Update the app that handles messages for a target. + * + * @param {string} target + * @param {TargetApp} app + * @param {unknown[]} [args] + * @returns {Promise} + */ + async reregister(target, app, args = []) { + const { targetHost } = this.facets; + const { appTransformer, targetToApp } = this.state; + + // Because wrapping an app is async, we verify absence of an existing + // registration twice (once to avoid the unnecessary invocation and + // once inside `set`), but attempt to throw similar errors in both + // cases. + targetToApp.has(target) || + Fail`Target ${target} is already unregistered`; + const wrappedApp = await (appTransformer + ? E(appTransformer).wrapApp(app, targetHost, ...args) + : app); + try { + targetToApp.set(target, wrappedApp); + } catch (cause) { + throw assert.error( + X`Target ${target} is already unregistered`, + undefined, + { cause }, + ); + } + }, + /** + * Unregister the target, bypassing the attenuated `TargetRegistration` + * API. + * + * @param {string} target + * @returns {Promise} + */ + async unregister(target) { + const { targetHost } = this.facets; + const { targetToApp } = this.state; + targetToApp.has(target) || Fail`Target ${target} is already deleted`; + targetToApp.delete(target); + await E(targetHost).sendDowncall({ + type: 'BRIDGE_TARGET_UNREGISTER', + target, + }); + }, + }, + }, + ); +harden(prepareBridgeTargetKit); + +/** @param {import('@agoric/base-zone').Zone} zone */ +export const prepareBridgeTargetModule = zone => { + const makeTargetRegistration = prepareTargetRegistration(zone); + const makeBridgeTargetKit = prepareBridgeTargetKit( + zone, + makeTargetRegistration, + ); + + return harden({ makeBridgeTargetKit }); +}; +harden(prepareBridgeTargetModule); diff --git a/packages/vats/src/core/types-ambient.d.ts b/packages/vats/src/core/types-ambient.d.ts index ec5043e8e29..e780c051637 100644 --- a/packages/vats/src/core/types-ambient.d.ts +++ b/packages/vats/src/core/types-ambient.d.ts @@ -381,14 +381,15 @@ type ChainBootstrapSpaceT = { storageBridgeManager: | import('../types.js').ScopedBridgeManager<'storage'> | undefined; + transferMiddleware: import('../transfer.js').TransferMiddleware; /** - * Convienence function for starting a contract (ungoverned) and saving its + * Convenience function for starting a contract (ungoverned) and saving its * facets (including adminFacet) */ startUpgradable: StartUpgradable; /** kits stored by startUpgradable */ contractKits: MapStore; - /** Convience function for starting contracts governed by the Econ Committee */ + /** Convenience function for starting contracts governed by the Econ Committee */ startGovernedUpgradable: StartGovernedUpgradable; /** kits stored by startGovernedUpgradable */ governedContractKits: MapStore< diff --git a/packages/vats/src/ibc.js b/packages/vats/src/ibc.js index e75e24afafc..c54ae476bce 100644 --- a/packages/vats/src/ibc.js +++ b/packages/vats/src/ibc.js @@ -3,7 +3,7 @@ import { assert, details as X, Fail } from '@agoric/assert'; import { E } from '@endo/far'; -import { dataToBase64, base64ToBytes } from '@agoric/network'; +import { byteSourceToBase64, base64ToBytes } from '@agoric/network'; import { localAddrToPortID, @@ -132,7 +132,7 @@ export const prepareIBCConnectionHandler = zone => { source_channel: channelID, destination_port: rPortID, destination_channel: rChannelID, - data: dataToBase64(packetBytes), + data: byteSourceToBase64(packetBytes), }; return protocolUtils.ibcSendPacket(packet, relativeTimeoutNs); }, @@ -678,7 +678,7 @@ export const prepareIBCProtocol = (zone, powers) => { const { packet } = watcherContext; const realAck = ack || DEFAULT_ACKNOWLEDGEMENT; - const ack64 = dataToBase64(realAck); + const ack64 = byteSourceToBase64(realAck); this.facets.util .downcall('receiveExecuted', { packet, diff --git a/packages/vats/src/localchain.js b/packages/vats/src/localchain.js index 1ec1441d6f8..6988eb1e2a0 100644 --- a/packages/vats/src/localchain.js +++ b/packages/vats/src/localchain.js @@ -15,6 +15,7 @@ const { Fail } = assert; * @typedef {{ * system: ScopedBridgeManager<'vlocalchain'>; * bank: Bank; + * transfer: import('./transfer.js').TransferMiddleware; * }} AccountPowers */ @@ -22,6 +23,7 @@ const { Fail } = assert; * @typedef {{ * system: ScopedBridgeManager<'vlocalchain'>; * bankManager: BankManager; + * transfer: import('./transfer.js').TransferMiddleware; * }} LocalChainPowers */ @@ -31,6 +33,9 @@ export const LocalChainAccountI = M.interface('LocalChainAccount', { deposit: M.callWhen(PaymentShape).optional(M.pattern()).returns(AmountShape), withdraw: M.callWhen(AmountShape).returns(PaymentShape), executeTx: M.callWhen(M.arrayOf(M.record())).returns(M.arrayOf(M.record())), + monitorTransfers: M.callWhen(M.remotable('TransferTap')).returns( + M.remotable('TargetRegistration'), + ), }); /** @param {import('@agoric/base-zone').Zone} zone */ @@ -105,6 +110,10 @@ const prepareLocalChainAccount = zone => }; return E(system).toBridge(obj); }, + async monitorTransfers(tap) { + const { address, transfer } = this.state; + return E(transfer).registerTap(address, tap); + }, }, ); /** @@ -139,12 +148,12 @@ const prepareLocalChain = (zone, makeAccount) => * hash and block data hash. */ async makeAccount() { - const { system, bankManager } = this.state; + const { system, bankManager, transfer } = this.state; const address = await E(system).toBridge({ type: 'VLOCALCHAIN_ALLOCATE_ADDRESS', }); const bank = await E(bankManager).getBankForAddress(address); - return makeAccount(address, { system, bank }); + return makeAccount(address, { system, bank, transfer }); }, /** * Make a single query to the local chain. Will reject with an error if diff --git a/packages/vats/src/proposals/localchain-proposal.js b/packages/vats/src/proposals/localchain-proposal.js index 3d640fdfbdc..68c9218ee8d 100644 --- a/packages/vats/src/proposals/localchain-proposal.js +++ b/packages/vats/src/proposals/localchain-proposal.js @@ -6,7 +6,10 @@ import { BridgeId as BRIDGE_ID } from '@agoric/internal'; * @param {BootstrapPowers & { * consume: { * loadCriticalVat: VatLoader; + * bridgeManager: import('../types').BridgeManager; * localchainBridgeManager: import('../types').ScopedBridgeManager<'vlocalchain'>; + * bankManager: Promise; + * transferMiddleware: Promise; * }; * produce: { * localchain: Producer; @@ -28,6 +31,7 @@ export const setupLocalChainVat = async ( bridgeManager: bridgeManagerP, localchainBridgeManager: localchainBridgeManagerP, bankManager, + transferMiddleware, }, produce: { localchainVat, localchain, localchainBridgeManager }, }, @@ -71,6 +75,7 @@ export const setupLocalChainVat = async ( const newLocalChain = await E(vats.localchain).makeLocalChain({ system: scopedManager, bankManager: await bankManager, + transfer: await transferMiddleware, }); localchain.reset(); diff --git a/packages/vats/src/proposals/localchain-test.js b/packages/vats/src/proposals/localchain-test.js index 41318e33bc9..7cdb756a656 100644 --- a/packages/vats/src/proposals/localchain-test.js +++ b/packages/vats/src/proposals/localchain-test.js @@ -19,13 +19,10 @@ export const testLocalChain = async ( /** @type {null | ERef} */ let node = await chainStorage; if (!node) { + console.error('testLocalChain no chainStorage'); throw new Error('no chainStorage'); } - for (const nodeName of testResultPath.split('.')) { - node = E(node).makeChildNode(nodeName); - } - let result; try { const lca = await E(localchain).makeAccount(); @@ -60,6 +57,9 @@ export const testLocalChain = async ( const emptyQuery = await E(localchain).queryMany([]); console.info('emptyQuery', emptyQuery); + if (emptyQuery.length !== 0) { + throw new Error('emptyQuery results should be empty'); + } result = { success: true }; } catch (e) { @@ -68,6 +68,9 @@ export const testLocalChain = async ( } console.warn('=== localchain test done, setting', { result }); + for (const nodeName of testResultPath.split('.')) { + node = E(node).makeChildNode(nodeName); + } await E(node).setValue(JSON.stringify(result)); }; diff --git a/packages/vats/src/proposals/restart-vats-proposal.js b/packages/vats/src/proposals/restart-vats-proposal.js index 9332626dae7..7613d833e51 100644 --- a/packages/vats/src/proposals/restart-vats-proposal.js +++ b/packages/vats/src/proposals/restart-vats-proposal.js @@ -17,9 +17,11 @@ const vatUpgradeStatus = { board: 'covered by test-upgrade-vats: upgrade vat-board', bridge: 'covered by test-upgrade-vats: upgrade vat-bridge', ibc: 'upgradeable', + localchain: 'UNTESTED', network: 'upgradeable', priceAuthority: 'covered by test-upgrade-vats: upgrade vat-priceAuthority', provisioning: 'UNTESTED', + transfer: 'UNTESTED', zoe: 'tested in @agoric/zoe', }; @@ -128,8 +130,10 @@ export const restartVats = async ({ consume }, { options }) => { console.log('upgrading vat', name); const { vatAdminSvc } = consume; const info = await consume.vatUpgradeInfo; - const { bundleID } = info.get(name); - const bcap = await E(vatAdminSvc).getBundleCap(bundleID); + const { bundleID, bundleName } = info.get(name); + const bcap = await (bundleID + ? E(vatAdminSvc).getBundleCap(bundleID) + : E(vatAdminSvc).getNamedBundleCap(bundleName)); await E(adminNode).upgrade(bcap); } console.log('VAT', name, status); diff --git a/packages/vats/src/proposals/transfer-proposal.js b/packages/vats/src/proposals/transfer-proposal.js new file mode 100644 index 00000000000..594bc3297ad --- /dev/null +++ b/packages/vats/src/proposals/transfer-proposal.js @@ -0,0 +1,120 @@ +// @ts-check +import { E } from '@endo/far'; +import { BridgeId as BRIDGE_ID, VTRANSFER_IBC_EVENT } from '@agoric/internal'; + +/** + * @param {BootstrapPowers & { + * consume: { + * loadCriticalVat: VatLoader; + * bridgeManager: import('../types').BridgeManager; + * vtransferBridgeManager: import('../types').ScopedBridgeManager<'vtransfer'>; + * }; + * produce: { + * transferMiddleware: Producer; + * transferVat: Producer; + * vtransferBridgeManager: Producer; + * }; + * }} powers + * @param {object} options + * @param {{ transferRef: VatSourceRef }} options.options + * + * @typedef {{ + * transfer: ERef; + * }} TransferVats + */ +export const setupTransferMiddleware = async ( + { + consume: { + loadCriticalVat, + bridgeManager: bridgeManagerP, + vtransferBridgeManager: vtransferBridgeManagerP, + }, + produce: { + transferMiddleware: produceTransferMiddleware, + transferVat: produceTransferVat, + vtransferBridgeManager: produceVtransferBridgeManager, + }, + }, + options, +) => { + const bridgeManager = await bridgeManagerP; + if (!bridgeManager) { + console.error('No bridgeManager, skipping setupTransferMiddleware'); + return; + } + + const { transferRef } = options.options; + /** @type {TransferVats} */ + const vats = { + transfer: E(loadCriticalVat)('transfer', transferRef), + }; + // don't proceed if loadCriticalVat fails + await Promise.all(Object.values(vats)); + produceTransferVat.reset(); + produceTransferVat.resolve(vats.transfer); + + // We'll be exporting a TransferMiddleware instance that implements a + // vtransfer app registry supporting active and passive "taps" while handling + // IBC transfer protocol conformance by configuring its backing + // BridgeTargetKit registry to wrap each app with an interceptor. + // But a bridge channel scoped to vtransfer might already exist, so we make or + // retrieve it as appropriate, then make its intercepting BridgeTargetKit, + // and then finally configure the TransferMiddleware with that kit's registry. + const { finisher, interceptorFactory, transferMiddleware } = await E( + vats.transfer, + ).makeTransferMiddlewareKit(); + const vtransferID = BRIDGE_ID.VTRANSFER; + const provideBridgeTargetKit = bridge => + E(vats.transfer).provideBridgeTargetKit( + bridge, + VTRANSFER_IBC_EVENT, + interceptorFactory, + ); + /** @type {Awaited>} */ + let bridgeTargetKit; + try { + const vtransferBridge = await E(bridgeManager).register(vtransferID); + produceVtransferBridgeManager.reset(); + produceVtransferBridgeManager.resolve(vtransferBridge); + bridgeTargetKit = await provideBridgeTargetKit(vtransferBridge); + await E(finisher).useRegistry(bridgeTargetKit.targetRegistry); + await E(vtransferBridge).initHandler(bridgeTargetKit.bridgeHandler); + console.info('Successfully initHandler for', vtransferID); + } catch (e) { + console.error( + 'Failed to initHandler', + vtransferID, + 'reason:', + e, + 'falling back to setHandler', + ); + const vtransferBridge = await vtransferBridgeManagerP; + bridgeTargetKit = await provideBridgeTargetKit(vtransferBridge); + await E(finisher).useRegistry(bridgeTargetKit.targetRegistry); + await E(vtransferBridge).setHandler(bridgeTargetKit.bridgeHandler); + console.info('Successfully setHandler for', vtransferID); + } + + produceTransferMiddleware.reset(); + produceTransferMiddleware.resolve(transferMiddleware); +}; + +export const getManifestForTransfer = (_powers, { transferRef }) => ({ + manifest: { + [setupTransferMiddleware.name]: { + consume: { + loadCriticalVat: true, + bridgeManager: 'bridge', + vtransferBridgeManager: 'transfer', + }, + produce: { + transferMiddleware: 'transfer', + transferVat: 'transfer', + vtransferBridgeManager: 'transfer', + }, + }, + }, + options: { + transferRef, + }, +}); diff --git a/packages/vats/src/proposals/vtransfer-echoer.js b/packages/vats/src/proposals/vtransfer-echoer.js new file mode 100644 index 00000000000..8993e3a6c7a --- /dev/null +++ b/packages/vats/src/proposals/vtransfer-echoer.js @@ -0,0 +1,42 @@ +// @ts-check +import { makeExo } from '@agoric/store'; +import { E } from '@endo/far'; + +/** + * @param {BootstrapPowers & { + * consume: { + * transferMiddleware: import('../transfer.js').TransferMiddleware; + * }; + * }} powers + * @param {object} options + * @param {{ target: string }} options.options + */ +export const echoVtransfer = async ( + { consume: { transferMiddleware } }, + { options: { target } }, +) => { + console.warn(`=== vtransfer echoer targeting ${target}`); + + // a tap that simply returns what it received + const tap = makeExo('echoer', undefined, { + // ack value must be stringlike + receiveUpcall: async param => JSON.stringify(param), + }); + + await E(transferMiddleware).registerActiveTap(target, tap); + + console.warn('=== vtransfer echoer registered'); +}; + +export const getManifestForVtransferEchoer = (_powers, { target }) => ({ + manifest: { + [echoVtransfer.name]: { + consume: { + transferMiddleware: 'transferMiddleware', + }, + }, + }, + options: { + target, + }, +}); diff --git a/packages/vats/src/transfer.js b/packages/vats/src/transfer.js new file mode 100644 index 00000000000..271b13489a2 --- /dev/null +++ b/packages/vats/src/transfer.js @@ -0,0 +1,271 @@ +// @ts-check +import { E } from '@endo/far'; +import { M } from '@endo/patterns'; +import { VTRANSFER_IBC_EVENT } from '@agoric/internal'; +import { coerceToByteSource, byteSourceToBase64 } from '@agoric/network'; +import { + prepareBridgeTargetModule, + TargetAppI, + AppTransformerI, +} from './bridge-target.js'; + +/** + * @import {TargetApp, TargetHost} from './bridge-target.js' + */ + +const { Fail, bare } = assert; + +/** + * The least possibly restrictive guard for a `watch` watcher's `onFulfilled` or + * `onRejected` reaction + */ +const ReactionGuard = M.call(M.any()).optional(M.any()).returns(M.any()); + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {import('@agoric/vow').VowTools} vowTools + */ +const prepareTransferInterceptor = (zone, vowTools) => { + const { watch } = vowTools; + const makeTransferInterceptorKit = zone.exoClassKit( + 'TransferInterceptorKit', + { + public: TargetAppI, + encodeAckWatcher: M.interface('EncodeAckWatcher', { + onFulfilled: ReactionGuard, + }), + sendErrorWatcher: M.interface('SendErrorWatcher', { + onRejected: ReactionGuard, + }), + logErrorWatcher: M.interface('LogErrorWatcher', { + onRejected: ReactionGuard, + }), + }, + /** + * @param {ERef} tap + * @param {ERef} targetHost + * @param {boolean} [isActiveTap] Whether the tap is active (can modify + * acknowledgements), or passive (can't cause delays in the middleware). + */ + (tap, targetHost, isActiveTap = false) => ({ + isActiveTap, + tap, + targetHost, + }), + { + public: { + async receiveUpcall(obj) { + const { isActiveTap, tap, targetHost } = this.state; + + obj.type === VTRANSFER_IBC_EVENT || + Fail`Invalid upcall argument type ${obj.type}; expected ${bare(VTRANSFER_IBC_EVENT)}`; + + // First, call our target contract listener. + // A VTransfer active interceptor can return a write acknowledgement + /** @type {import('@agoric/vow').Vow} */ + let retP = watch(E(tap).receiveUpcall(obj)); + + // See if the upcall result needs special handling. + if (obj.event === 'writeAcknowledgement') { + const ackMethodData = { + type: 'IBC_METHOD', + method: 'receiveExecuted', + packet: obj.packet, + }; + if (isActiveTap) { + retP = watch(retP, this.facets.encodeAckWatcher, { + ackMethodData, + }); + } else { + // This is a passive tap, so forward the ack without intervention. + ackMethodData.ack = obj.acknowledgement; + retP = watch(E(targetHost).sendDowncall(ackMethodData)); + } + retP = watch(retP, this.facets.sendErrorWatcher, { ackMethodData }); + } + + // Log errors in the upcall handling. + retP = watch(retP, this.facets.logErrorWatcher, { obj }); + + if (isActiveTap) { + // If the tap is active, return the promiseVow to the caller. This + // will delay the middleware until fulfilment of the retP chain. + return retP; + } + + // Otherwise, passively return nothing. + }, + }, + /** + * `watch` callback for encoding the raw `ack` return value from an active + * `writeAcknowledgement` tap as base64, then sending down to the + * targetHost. + */ + encodeAckWatcher: { + onFulfilled(rawAck, { ackMethodData }) { + // Encode the tap's ack and write it out. + const ack = byteSourceToBase64(coerceToByteSource(rawAck)); + ackMethodData = { ...ackMethodData, ack }; + return E(this.state.targetHost).sendDowncall(ackMethodData); + }, + }, + /** + * `watch` callback for handling errors in the sending of an ack and + * reifying it as an error acknowledgement. + */ + sendErrorWatcher: { + onRejected(error, { ackMethodData }) { + console.error(`Error sending ack:`, error); + const rawAck = JSON.stringify({ error: error.message }); + ackMethodData = { ...ackMethodData, ack: byteSourceToBase64(rawAck) }; + return E(this.state.targetHost).sendDowncall(ackMethodData); + }, + }, + /** `watch` callback for logging errors in the upcall handling. */ + logErrorWatcher: { + onRejected(error, { obj }) { + console.error(`Error in handling of`, obj, error); + }, + }, + }, + ); + + /** + * A TransferInterceptor wraps a TargetApp that is specialized for handling + * asynchronous callbacks from the IBC transfer protocol as implemented by + * `x/vtransfer`. By using this interceptor, the TargetApp is prevented from + * sending messages which may violate the transfer protocol directly to the + * `targetHost`. + * + * @param {Parameters} args + */ + const makeTransferInterceptor = (...args) => + makeTransferInterceptorKit(...args).public; + return makeTransferInterceptor; +}; +harden(prepareTransferInterceptor); + +const TransferMiddlewareKitFinisherI = M.interface( + 'TransferMiddlewareKitFinisher', + { + useRegistry: M.call(M.remotable('TargetRegistry')).returns(), + }, +); + +const TransferMiddlewareI = M.interface('TransferMiddleware', { + registerTap: M.callWhen( + M.string(), + M.remotable('TransferInterceptor'), + ).returns(M.remotable('TargetRegistration')), + registerActiveTap: M.callWhen( + M.string(), + M.remotable('TransferInterceptor'), + ).returns(M.remotable('TargetRegistration')), + unregisterTap: M.callWhen(M.string()).returns(), +}); + +/** + * @callback RegisterTap + * @param {string} target String identifying the bridge target. + * @param {ERef} tap The "application + * tap" to register for the target. + */ + +/** + * A TransferMiddlewareKit has a `transferMiddleware` facet with methods for + * registering and unregistering active and passive "taps" in a BridgeTargetKit + * registry that must use the `interceptorFactory` facet as its appTransfomer to + * enforce IBC transfer protocol conformance. Before registering any taps, the + * backing BridgeTargetKit must be set with the `finisher` facet (i.e., the + * expected setup pattern is to pass in `interceptorFactory` as the + * appTransformer argument when making a BridgeTargetKit and then to invoke + * `finisher.useRegistry` with the `targetRegistry` of that BridgeTargetKit + * before making further use of the TransferMiddlewareKit or connecting the + * BridgeTargetKit to a bridge). + * + * @param {import('@agoric/base-zone').Zone} zone + * @param {ReturnType} makeTransferInterceptor + */ +const prepareTransferMiddlewareKit = (zone, makeTransferInterceptor) => + zone.exoClassKit( + 'TransferMiddlewareKit', + { + finisher: TransferMiddlewareKitFinisherI, + interceptorFactory: AppTransformerI, + transferMiddleware: TransferMiddlewareI, + }, + () => ({ + /** @type {import('./bridge-target').TargetRegistry | undefined} */ + targetRegistry: undefined, + }), + { + finisher: { + /** + * @param {import('./bridge-target').TargetRegistry} registry + */ + useRegistry(registry) { + this.state.targetRegistry = registry; + }, + }, + interceptorFactory: { + wrapApp: makeTransferInterceptor, + }, + transferMiddleware: { + /** + * Register a tap to intercept the vtransfer target account address + * messages, without granting that tap the ability to delay or interfere + * with the underlying IBC transfer protocol. + * + * @type {RegisterTap} + */ + async registerTap(target, tap) { + const { targetRegistry } = this.state; + if (!targetRegistry) throw Fail`Registry not initialized`; + return E(targetRegistry).register(target, tap, []); + }, + /** + * Similar to `registerTap`, but allows the tap to inject async + * acknowledgements in the IBC transfer protocol. + * + * @type {RegisterTap} + */ + async registerActiveTap(target, tap) { + const { targetRegistry } = this.state; + if (!targetRegistry) throw Fail`Registry not initialized`; + return E(targetRegistry).register(target, tap, [true]); + }, + /** + * Unregister the target. + * + * @param {string} target String identifying the bridge target to stop + * tapping. + */ + async unregisterTap(target) { + const { targetRegistry } = this.state; + if (!targetRegistry) throw Fail`Registry not initialized`; + await E(targetRegistry).unregister(target); + }, + }, + }, + ); +/** @typedef {ReturnType>} TransferMiddlewareKit */ +/** @typedef {TransferMiddlewareKit['transferMiddleware']} TransferMiddleware */ + +/** + * @param {import('@agoric/base-zone').Zone} zone + * @param {import('@agoric/vow').VowTools} vowTools + */ +export const prepareTransferTools = (zone, vowTools) => { + const makeTransferInterceptor = prepareTransferInterceptor(zone, vowTools); + const { makeBridgeTargetKit } = prepareBridgeTargetModule( + zone.subZone('bridge-target'), + ); + + const makeTransferMiddlewareKit = prepareTransferMiddlewareKit( + zone, + makeTransferInterceptor, + ); + + return harden({ makeTransferMiddlewareKit, makeBridgeTargetKit }); +}; +harden(prepareTransferTools); diff --git a/packages/vats/src/types.d.ts b/packages/vats/src/types.d.ts index 11613498f5f..f8c9dde5570 100644 --- a/packages/vats/src/types.d.ts +++ b/packages/vats/src/types.d.ts @@ -1,6 +1,5 @@ import type { BridgeIdValue, Remote } from '@agoric/internal'; import type { Bytes } from '@agoric/network'; -import type { PromiseVow } from '@agoric/vow'; import type { Guarded } from '@endo/exo'; export type Board = ReturnType< @@ -92,7 +91,7 @@ export type NamesByAddressAdmin = NameAdmin & { /** An object that can receive messages from the bridge device */ export type BridgeHandler = { /** Handle an inbound message */ - fromBridge: (obj: any) => PromiseVow; + fromBridge: (obj: any) => Promise; }; /** An object which handles messages for a specific bridge */ @@ -103,8 +102,10 @@ export type ScopedBridgeManager = Guarded<{ * system to hang the bridgeId */ getBridgeId?: () => BridgeId; + /** Downcall from the VM into Golang */ toBridge: (obj: any) => Promise; - fromBridge: (obj: any) => PromiseVow; + /** Upcall from Golang into the VM */ + fromBridge: (obj: any) => Promise; initHandler: (handler: Remote) => void; setHandler: (handler: Remote) => void; }>; diff --git a/packages/vats/src/vat-transfer.js b/packages/vats/src/vat-transfer.js new file mode 100644 index 00000000000..b3a78973ae8 --- /dev/null +++ b/packages/vats/src/vat-transfer.js @@ -0,0 +1,63 @@ +// @ts-check +import { Far } from '@endo/far'; +import { makeDurableZone } from '@agoric/zone/durable.js'; + +import { provideLazy } from '@agoric/store'; +import { prepareVowTools } from '@agoric/vat-data/vow.js'; +import { prepareBridgeTargetModule } from './bridge-target.js'; +import { prepareTransferTools } from './transfer.js'; + +export const buildRootObject = (_vatPowers, _args, baggage) => { + const zone = makeDurableZone(baggage); + + const { makeBridgeTargetKit } = prepareBridgeTargetModule( + zone.subZone('bridge'), + ); + + const vowTools = prepareVowTools(zone.subZone('vow')); + + const { makeTransferMiddlewareKit } = prepareTransferTools( + zone.subZone('transfer'), + vowTools, + ); + + /** + * This 2-level structure is to avoid holding the bridge managers strongly, as + * well as accommodate the lack of complex keys. + * + * @type {WeakMapStore< + * import('./types').ScopedBridgeManager, + * MapStore> + * >} + */ + const managerToKits = zone.weakMapStore('managerToHandler'); + return Far('TransferVat', { + /** + * @template {import('@agoric/internal').BridgeIdValue} T + * @param {import('./types').ScopedBridgeManager} manager + * @param {string} [inboundType] + * @param {import('./bridge-target').AppTransformer} [appTransformer] + */ + provideBridgeTargetKit( + manager, + inboundType = 'IBC_EVENT', + appTransformer = undefined, + ) { + /** @type {MapStore>} */ + const inboundTypeToKit = provideLazy(managerToKits, manager, () => + zone.detached().mapStore('inboundTypeToKit'), + ); + const kit = provideLazy(inboundTypeToKit, inboundType, () => + makeBridgeTargetKit(manager, inboundType, appTransformer), + ); + return kit; + }, + /** + * Create middleware for exposing IBC messages to and from the underlying + * vtransfer port as data with embedded `action` ocaps where safe. + */ + makeTransferMiddlewareKit, + }); +}; + +/** @typedef {ReturnType} TransferVat */ diff --git a/packages/vats/test/localchain.test.js b/packages/vats/test/localchain.test.js index 4253b72e9d5..4c96a44d8c1 100644 --- a/packages/vats/test/localchain.test.js +++ b/packages/vats/test/localchain.test.js @@ -8,11 +8,15 @@ import { withAmountUtils } from '@agoric/zoe/tools/test-utils.js'; import { makeDurableZone } from '@agoric/zone/durable.js'; import { E } from '@endo/far'; import { getInterfaceOf } from '@endo/marshal'; +import { VTRANSFER_IBC_EVENT } from '@agoric/internal'; +import { prepareVowTools } from '@agoric/vow/vat.js'; import { prepareLocalChainTools } from '../src/localchain.js'; +import { prepareTransferTools } from '../src/transfer.js'; import { makeFakeBankManagerKit } from '../tools/bank-utils.js'; import { LOCALCHAIN_DEFAULT_ADDRESS, makeFakeLocalchainBridge, + makeFakeTransferBridge, } from '../tools/fake-bridge.js'; /** @@ -40,6 +44,21 @@ const makeTestContext = async _t => { makeDurableZone(provideBaggage('localchain')), ); + const transferZone = makeDurableZone(provideBaggage('transfer')); + const transferBridge = makeFakeTransferBridge(transferZone.subZone('bridge')); + const transferTools = prepareTransferTools( + transferZone, + prepareVowTools(transferZone.subZone('vows')), + ); + const { finisher, interceptorFactory, transferMiddleware } = + transferTools.makeTransferMiddlewareKit(); + const bridgeTargetKit = transferTools.makeBridgeTargetKit( + transferBridge, + VTRANSFER_IBC_EVENT, + interceptorFactory, + ); + finisher.useRegistry(bridgeTargetKit.targetRegistry); + const { bankManager, pourPayment } = await makeFakeBankManagerKit({ balances: { // agoric1fakeBridgeAddress: { ubld: bld.units(100).value }, @@ -55,6 +74,7 @@ const makeTestContext = async _t => { const localchain = await makeLocalChain({ system: localchainBridge, bankManager, + transfer: transferMiddleware, }); return { diff --git a/packages/vats/tools/fake-bridge.js b/packages/vats/tools/fake-bridge.js index e6e5e621098..9976648508e 100644 --- a/packages/vats/tools/fake-bridge.js +++ b/packages/vats/tools/fake-bridge.js @@ -1,3 +1,4 @@ +// @ts-check import { assert, Fail } from '@agoric/assert'; import { makeTracer, VBankAccount } from '@agoric/internal'; import { E } from '@endo/far'; @@ -222,3 +223,47 @@ export const makeFakeLocalchainBridge = (zone, onToBridge = () => {}) => { }, }); }; + +/** + * @param {import('@agoric/zone').Zone} zone + * @param {(obj) => void} [onToBridge] + * @returns {ScopedBridgeManager<'vtransfer'>} + */ +export const makeFakeTransferBridge = (zone, onToBridge = () => {}) => { + /** @type {Remote} */ + let hndlr; + const registered = zone.setStore('registered'); + return zone.exo('Fake Transfer Bridge Manager', undefined, { + getBridgeId: () => 'vtransfer', + toBridge: async obj => { + onToBridge(obj); + const { type, ...params } = obj; + trace('toBridge', type, params); + switch (type) { + case 'BRIDGE_TARGET_REGISTER': { + registered.add(params.target); + return undefined; + } + case 'BRIDGE_TARGET_UNREGISTER': { + registered.delete(params.target); + return undefined; + } + default: + Fail`unknown type ${type}`; + } + return undefined; + }, + fromBridge: async obj => { + if (!hndlr) throw Error('no handler!'); + return when(E(hndlr).fromBridge(obj)); + }, + initHandler: h => { + if (hndlr) throw Error('already init'); + hndlr = h; + }, + setHandler: h => { + if (!hndlr) throw Error('must init first'); + hndlr = h; + }, + }); +}; diff --git a/packages/vm-config/decentral-demo-config.json b/packages/vm-config/decentral-demo-config.json index 7daa9b30ac7..4137e94ee72 100644 --- a/packages/vm-config/decentral-demo-config.json +++ b/packages/vm-config/decentral-demo-config.json @@ -7,6 +7,7 @@ "@agoric/builders/scripts/vats/init-core.js", "@agoric/builders/scripts/vats/init-network.js", "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js", { "module": "@agoric/builders/scripts/inter-protocol/init-core.js", "entrypoint": "defaultProposalBuilder", diff --git a/packages/vm-config/decentral-devnet-config.json b/packages/vm-config/decentral-devnet-config.json index 2bf94fdd6f4..e93ba27a609 100644 --- a/packages/vm-config/decentral-devnet-config.json +++ b/packages/vm-config/decentral-devnet-config.json @@ -9,7 +9,8 @@ ], [ "@agoric/builders/scripts/vats/init-network.js", - "@agoric/builders/scripts/vats/init-localchain.js" + "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js" ], [ "@agoric/builders/scripts/vats/init-orchestration.js" diff --git a/packages/vm-config/decentral-itest-vaults-config.json b/packages/vm-config/decentral-itest-vaults-config.json index c2b63b11159..4e3dc96ae75 100644 --- a/packages/vm-config/decentral-itest-vaults-config.json +++ b/packages/vm-config/decentral-itest-vaults-config.json @@ -4,6 +4,9 @@ "defaultReapInterval": 1000, "coreProposals": [ "@agoric/builders/scripts/vats/init-core.js", + "@agoric/builders/scripts/vats/init-network.js", + "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js", { "module": "@agoric/builders/scripts/inter-protocol/init-core.js", "entrypoint": "defaultProposalBuilder", diff --git a/packages/vm-config/decentral-main-vaults-config.json b/packages/vm-config/decentral-main-vaults-config.json index a5676dd0bc8..19aa71a0911 100644 --- a/packages/vm-config/decentral-main-vaults-config.json +++ b/packages/vm-config/decentral-main-vaults-config.json @@ -4,6 +4,9 @@ "defaultReapInterval": 1000, "coreProposals": [ "@agoric/builders/scripts/vats/init-core.js", + "@agoric/builders/scripts/vats/init-network.js", + "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js", { "module": "@agoric/builders/scripts/inter-protocol/init-core.js", "entrypoint": "defaultProposalBuilder", diff --git a/packages/vm-config/demo-proposals.json b/packages/vm-config/demo-proposals.json index c50ad404e82..df3895f5e1a 100644 --- a/packages/vm-config/demo-proposals.json +++ b/packages/vm-config/demo-proposals.json @@ -2,5 +2,6 @@ "@agoric/builders/scripts/inter-protocol/init-core.js", "@agoric/builders/scripts/pegasus/init-core.js", "@agoric/builders/scripts/vats/init-network.js", - "@agoric/builders/scripts/vats/init-localchain.js" + "@agoric/builders/scripts/vats/init-localchain.js", + "@agoric/builders/scripts/vats/init-transfer.js" ] diff --git a/packages/vow/src/tools.js b/packages/vow/src/tools.js index 1abfe599b37..020298a6056 100644 --- a/packages/vow/src/tools.js +++ b/packages/vow/src/tools.js @@ -19,6 +19,11 @@ export const prepareVowTools = (zone, powers = {}) => { const makeWatchUtils = prepareWatchUtils(zone, watch, makeVowKit); const watchUtils = makeWatchUtils(); + /** + * Vow-tolerant implementation of Promise.all. + * + * @param {unknown[]} vows + */ const allVows = vows => watchUtils.all(vows); return harden({ when, watch, makeVowKit, allVows }); diff --git a/packages/vow/src/types.js b/packages/vow/src/types.js index 974e28bb796..bccf77bec71 100644 --- a/packages/vow/src/types.js +++ b/packages/vow/src/types.js @@ -66,15 +66,6 @@ export {}; * }} VowKit */ -/** - * @template [T=any] - * @typedef {{ - * vow: Vow, - * resolver: VowResolver, - * promise: Promise - * }} VowPromiseKit - */ - /** * @template [T=any] * @typedef {{ resolve(value?: T | PromiseVow): void, reject(reason?: any): void }} VowResolver @@ -83,16 +74,11 @@ export {}; /** * @template [T=any] * @template [TResult1=T] - * @template [TResult2=T] + * @template [TResult2=never] * @template [C=any] watcher context * @typedef {object} Watcher * @property {(value: T, context?: C) => Vow | PromiseVow | TResult1} [onFulfilled] * @property {(reason: any) => Vow | PromiseVow | TResult2} [onRejected] */ -/** - * @template [T=any] - * @typedef {ERef>} Specimen - */ - /** @typedef {ReturnType} VowTools */ diff --git a/packages/vow/src/watch-utils.js b/packages/vow/src/watch-utils.js index ab2d1f8a587..7d9334b143f 100644 --- a/packages/vow/src/watch-utils.js +++ b/packages/vow/src/watch-utils.js @@ -6,7 +6,7 @@ import { M } from '@endo/patterns'; * @import {MapStore} from '@agoric/store/src/types.js' * @import { Zone } from '@agoric/base-zone' * @import { Watch } from './watch.js' - * @import {Specimen, VowKit} from './types.js' + * @import {VowKit} from './types.js' */ const VowShape = M.tagged( @@ -52,11 +52,11 @@ export const prepareWatchUtils = (zone, watch, makeVowKit) => { { utils: { /** - * @template [T=any] - * @param {Specimen[]} vows + * @param {unknown[]} vows */ all(vows) { const { nextId: id, idToVowState } = this.state; + /** @type {VowKit} */ const kit = makeVowKit(); // Preserve the order of the vow results. diff --git a/packages/vow/src/watch.js b/packages/vow/src/watch.js index 2b87e41e219..a7bb6736aad 100644 --- a/packages/vow/src/watch.js +++ b/packages/vow/src/watch.js @@ -132,16 +132,18 @@ export const prepareWatch = ( ); /** - * @template [T=unknown] + * @template [T=any] * @template [TResult1=T] - * @template [TResult2=T] - * @template [C=unknown] watcher context + * @template [TResult2=never] + * @template [C=any] watcher context * @param {ERef>} specimenP * @param {Watcher} [watcher] * @param {C} [watcherContext] */ const watch = (specimenP, watcher, watcherContext) => { - /** @type {VowKit} */ + /** @typedef {Exclude | Exclude} Voidless */ + /** @typedef {Voidless extends never ? TResult1 : Voidless} Narrowest */ + /** @type {VowKit} */ const { resolver, vow } = makeVowKit(); // Create a promise watcher to track vows, retrying upon rejection as diff --git a/packages/vow/vat.js b/packages/vow/vat.js index f4d268f3166..a677ee42eb6 100644 --- a/packages/vow/vat.js +++ b/packages/vow/vat.js @@ -21,8 +21,8 @@ export const defaultPowers = harden({ export const prepareVowTools = (zone, powers = {}) => rawPrepareVowTools(zone, { ...defaultPowers, ...powers }); -export const { watch, when, makeVowKit, allVows } = - prepareVowTools(makeHeapZone()); +export const vowTools = prepareVowTools(makeHeapZone()); +export const { watch, when, makeVowKit, allVows } = vowTools; /** * A vow-shortening E. CAVEAT: This produces long-lived ephemeral