From c72a31e4ac4cc52286e797129445b43c6c8e7bb2 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 23 Nov 2023 18:27:39 +0100 Subject: [PATCH] feat: add 'gnokey maketx run' (#1001) --- gno.land/Makefile | 3 + gno.land/cmd/gnoland/testdata/addpkg.txtar | 4 +- gno.land/cmd/gnoland/testdata/run.txtar | 28 +++++ gno.land/pkg/sdk/vm/handler.go | 21 ++++ gno.land/pkg/sdk/vm/keeper.go | 86 +++++++++++++ gno.land/pkg/sdk/vm/keeper_test.go | 58 +++++++++ gno.land/pkg/sdk/vm/msgs.go | 64 ++++++++++ gno.land/pkg/sdk/vm/package.go | 1 + tm2/pkg/crypto/keys/client/maketx.go | 1 + tm2/pkg/crypto/keys/client/run.go | 139 +++++++++++++++++++++ 10 files changed, 402 insertions(+), 3 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/run.txtar create mode 100644 tm2/pkg/crypto/keys/client/run.go diff --git a/gno.land/Makefile b/gno.land/Makefile index 29c192e9987..1297da393cb 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -18,6 +18,9 @@ build.gnokey:; go build -o build/gnokey ./cmd/gnokey build.gnotxsync:; go build -o build/gnotxsync ./cmd/gnotxsync build.genesis:; go build -o build/genesis ./cmd/genesis +run.gnoland:; go run ./cmd/gnoland start +run.gnoweb:; go run ./cmd/gnoweb + .PHONY: install install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.gnotxsync install.genesis diff --git a/gno.land/cmd/gnoland/testdata/addpkg.txtar b/gno.land/cmd/gnoland/testdata/addpkg.txtar index 5f1ee0caf49..7130fe54dab 100644 --- a/gno.land/cmd/gnoland/testdata/addpkg.txtar +++ b/gno.land/cmd/gnoland/testdata/addpkg.txtar @@ -15,11 +15,9 @@ stdout 'OK!' stdout 'GAS WANTED: 2000000' stdout 'GAS USED: [0-9]+' - -- bar.gno -- package bar func Render(path string) string { return "hello from foo" -} - +} \ No newline at end of file diff --git a/gno.land/cmd/gnoland/testdata/run.txtar b/gno.land/cmd/gnoland/testdata/run.txtar new file mode 100644 index 00000000000..94b32de041e --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/run.txtar @@ -0,0 +1,28 @@ +## start a new node +gnoland start + +## add bar.gno package located in $WORK directory as gno.land/r/foobar/bar +gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/foobar/bar -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 + +## execute Render +gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 $WORK/script/script.gno + +## compare render +stdout 'main: --- hello from foo ---' +stdout 'OK!' +stdout 'GAS WANTED: 200000' +stdout 'GAS USED: ' + +-- bar/bar.gno -- +package bar + +func Render(path string) string { + return "hello from foo" +} + +-- script/script.gno -- +package main +import "gno.land/r/foobar/bar" +func main() { + println("main: ---", bar.Render(""), "---") +} diff --git a/gno.land/pkg/sdk/vm/handler.go b/gno.land/pkg/sdk/vm/handler.go index accaa70e059..6c3a97696d6 100644 --- a/gno.land/pkg/sdk/vm/handler.go +++ b/gno.land/pkg/sdk/vm/handler.go @@ -27,6 +27,8 @@ func (vh vmHandler) Process(ctx sdk.Context, msg std.Msg) sdk.Result { return vh.handleMsgAddPackage(ctx, msg) case MsgCall: return vh.handleMsgCall(ctx, msg) + case MsgRun: + return vh.handleMsgRun(ctx, msg) default: errMsg := fmt.Sprintf("unrecognized vm message type: %T", msg) return abciResult(std.ErrUnknownRequest(errMsg)) @@ -77,6 +79,25 @@ func (vh vmHandler) handleMsgCall(ctx sdk.Context, msg MsgCall) (res sdk.Result) */ } +// Handle MsgRun. +func (vh vmHandler) handleMsgRun(ctx sdk.Context, msg MsgRun) (res sdk.Result) { + amount, err := std.ParseCoins("1000000ugnot") // XXX calculate + if err != nil { + return abciResult(err) + } + err = vh.vm.bank.SendCoins(ctx, msg.Caller, auth.FeeCollectorAddress(), amount) + if err != nil { + return abciResult(err) + } + resstr := "" + resstr, err = vh.vm.Run(ctx, msg) + if err != nil { + return abciResult(err) + } + res.Data = []byte(resstr) + return +} + //---------------------------------------- // Query diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 6f695e98558..b0ae2180c36 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -3,8 +3,10 @@ package vm // TODO: move most of the logic in ROOT/gno.land/... import ( + "bytes" "fmt" "os" + "regexp" "strings" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" @@ -27,6 +29,7 @@ const ( type VMKeeperI interface { AddPackage(ctx sdk.Context, msg MsgAddPackage) error Call(ctx sdk.Context, msg MsgCall) (res string, err error) + Run(ctx sdk.Context, msg MsgRun) (res string, err error) } var _ VMKeeperI = &VMKeeper{} @@ -128,6 +131,10 @@ func (vm *VMKeeper) getGnoStore(ctx sdk.Context) gno.Store { } } +const ( + reReservedPath = `gno\.land/r/g[a-z0-9]+/run` +) + // AddPackage adds a package with given fileset. func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { creator := msg.Creator @@ -150,6 +157,11 @@ func (vm *VMKeeper) AddPackage(ctx sdk.Context, msg MsgAddPackage) error { if pv := store.GetPackage(pkgPath, false); pv != nil { return ErrInvalidPkgPath("package already exists: " + pkgPath) } + + if ok, _ := regexp.MatchString(reReservedPath, pkgPath); ok { + return ErrInvalidPkgPath("reserved package name: " + pkgPath) + } + // Pay deposit from creator. pkgAddr := gno.DerivePkgAddr(pkgPath) @@ -282,6 +294,80 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { // TODO pay for gas? TODO see context? } +// Run executes arbitrary Gno code in the context of the caller's realm. +func (vm *VMKeeper) Run(ctx sdk.Context, msg MsgRun) (res string, err error) { + caller := msg.Caller + pkgAddr := caller + store := vm.getGnoStore(ctx) + send := msg.Send + memPkg := msg.Package + + // Validate arguments. + callerAcc := vm.acck.GetAccount(ctx, caller) + if callerAcc == nil { + return "", std.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", caller)) + } + if err := msg.Package.Validate(); err != nil { + return "", ErrInvalidPkgPath(err.Error()) + } + + // Send send-coins to pkg from caller. + err = vm.bank.SendCoins(ctx, caller, pkgAddr, send) + if err != nil { + return "", err + } + + // Parse and run the files, construct *PV. + msgCtx := stdlibs.ExecContext{ + ChainID: ctx.ChainID(), + Height: ctx.BlockHeight(), + Timestamp: ctx.BlockTime().Unix(), + Msg: msg, + OrigCaller: caller.Bech32(), + OrigSend: send, + OrigSendSpent: new(std.Coins), + OrigPkgAddr: pkgAddr.Bech32(), + Banker: NewSDKBanker(vm, ctx), + } + // Parse and run the files, construct *PV. + buf := new(bytes.Buffer) + m := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: "", + Output: buf, + Store: store, + Alloc: store.GetAllocator(), + Context: msgCtx, + MaxCycles: vm.maxCycles, + }) + defer m.Release() + _, pv := m.RunMemPackage(memPkg, false) + ctx.Logger().Info("CPUCYCLES", "addpkg", m.Cycles) + + m2 := gno.NewMachineWithOptions( + gno.MachineOptions{ + PkgPath: "", + Output: buf, + Store: store, + Alloc: store.GetAllocator(), + Context: msgCtx, + MaxCycles: vm.maxCycles, + }) + m2.SetActivePackage(pv) + defer func() { + if r := recover(); r != nil { + err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\n%s\n", + r, m2.String()) + return + } + m2.Release() + }() + m2.RunMain() + ctx.Logger().Info("CPUCYCLES call: ", m2.Cycles) + res = buf.String() + return res, nil +} + // QueryFuncs returns public facing function signatures. func (vm *VMKeeper) QueryFuncs(ctx sdk.Context, pkgPath string) (fsigs FunctionSignatures, err error) { store := vm.getGnoStore(ctx) diff --git a/gno.land/pkg/sdk/vm/keeper_test.go b/gno.land/pkg/sdk/vm/keeper_test.go index 27a1054e914..294efa66fa5 100644 --- a/gno.land/pkg/sdk/vm/keeper_test.go +++ b/gno.land/pkg/sdk/vm/keeper_test.go @@ -337,3 +337,61 @@ func GetAdmin() string { assert.NoError(t, err) assert.Equal(t, res, addrString) } + +// Call Run without imports, without variables. +func TestVMKeeperRunSimple(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + + files := []*std.MemFile{ + {"script.gno", ` +package main + +func main() { + println("hello world!") +} +`}, + } + + coins := std.MustParseCoins("") + msg2 := NewMsgRun(addr, coins, files) + res, err := env.vmk.Run(ctx, msg2) + assert.NoError(t, err) + assert.Equal(t, res, "hello world!\n") +} + +// Call Run with stdlibs. +func TestVMKeeperRunImportStdlibs(t *testing.T) { + env := setupTestEnv() + ctx := env.ctx + + // Give "addr1" some gnots. + addr := crypto.AddressFromPreimage([]byte("addr1")) + acc := env.acck.NewAccountWithAddress(ctx, addr) + env.acck.SetAccount(ctx, acc) + + files := []*std.MemFile{ + {"script.gno", ` +package main + +import "std" + +func main() { + addr := std.GetOrigCaller() + println("hello world!", addr) +} +`}, + } + + coins := std.MustParseCoins("") + msg2 := NewMsgRun(addr, coins, files) + res, err := env.vmk.Run(ctx, msg2) + assert.NoError(t, err) + expectedString := fmt.Sprintf("hello world! %s\n", addr.String()) + assert.Equal(t, res, expectedString) +} diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 3cfc6c58224..f1e65ae25cb 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -135,3 +135,67 @@ func (msg MsgCall) GetSigners() []crypto.Address { func (msg MsgCall) GetReceived() std.Coins { return msg.Send } + +//---------------------------------------- +// MsgRun + +// MsgRun - executes arbitrary Gno code. +type MsgRun struct { + Caller crypto.Address `json:"caller" yaml:"caller"` + Send std.Coins `json:"send" yaml:"send"` + Package *std.MemPackage `json:"package" yaml:"package"` +} + +var _ std.Msg = MsgRun{} + +func NewMsgRun(caller crypto.Address, send std.Coins, files []*std.MemFile) MsgRun { + for _, file := range files { + if strings.HasSuffix(file.Name, ".gno") { + pkgName := string(gno.PackageNameFromFileBody(file.Name, file.Body)) + if pkgName != "main" { + panic("package name should be 'main'") + } + } + } + return MsgRun{ + Caller: caller, + Send: send, + Package: &std.MemPackage{ + Name: "main", + Path: "gno.land/r/" + caller.String() + "/run", + Files: files, + }, + } +} + +// Implements Msg. +func (msg MsgRun) Route() string { return RouterKey } + +// Implements Msg. +func (msg MsgRun) Type() string { return "run" } + +// Implements Msg. +func (msg MsgRun) ValidateBasic() error { + if msg.Caller.IsZero() { + return std.ErrInvalidAddress("missing caller address") + } + if msg.Package.Path == "" { // XXX + return ErrInvalidPkgPath("missing package path") + } + return nil +} + +// Implements Msg. +func (msg MsgRun) GetSignBytes() []byte { + return std.MustSortJSON(amino.MustMarshalJSON(msg)) +} + +// Implements Msg. +func (msg MsgRun) GetSigners() []crypto.Address { + return []crypto.Address{msg.Caller} +} + +// Implements ReceiveMsg. +func (msg MsgRun) GetReceived() std.Coins { + return msg.Send +} diff --git a/gno.land/pkg/sdk/vm/package.go b/gno.land/pkg/sdk/vm/package.go index 5d05c108bd0..01fad3284e3 100644 --- a/gno.land/pkg/sdk/vm/package.go +++ b/gno.land/pkg/sdk/vm/package.go @@ -13,6 +13,7 @@ var Package = amino.RegisterPackage(amino.NewPackage( std.Package, ).WithTypes( MsgCall{}, "m_call", + MsgRun{}, "m_run", MsgAddPackage{}, "m_addpkg", // TODO rename both to MsgAddPkg? // errors diff --git a/tm2/pkg/crypto/keys/client/maketx.go b/tm2/pkg/crypto/keys/client/maketx.go index e3fe89b930d..c424b566c95 100644 --- a/tm2/pkg/crypto/keys/client/maketx.go +++ b/tm2/pkg/crypto/keys/client/maketx.go @@ -36,6 +36,7 @@ func newMakeTxCmd(rootCfg *baseCfg, io commands.IO) *commands.Command { newAddPkgCmd(cfg, io), newSendCmd(cfg, io), newCallCmd(cfg, io), + newRunCmd(cfg, io), ) return cmd diff --git a/tm2/pkg/crypto/keys/client/run.go b/tm2/pkg/crypto/keys/client/run.go new file mode 100644 index 00000000000..1ae702c990e --- /dev/null +++ b/tm2/pkg/crypto/keys/client/run.go @@ -0,0 +1,139 @@ +package client + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "os" + + "github.com/gnolang/gno/gno.land/pkg/sdk/vm" + gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/tm2/pkg/amino" + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/std" +) + +type runCfg struct { + rootCfg *makeTxCfg +} + +func newRunCmd(rootCfg *makeTxCfg, io commands.IO) *commands.Command { + cfg := &runCfg{ + rootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "run", + ShortUsage: "run [flags] ", + ShortHelp: "Runs Gno code by invoking main() in a package", + }, + cfg, + func(_ context.Context, args []string) error { + return runRun(cfg, args, io) + }, + ) +} + +func (c *runCfg) RegisterFlags(fs *flag.FlagSet) {} + +func runRun(cfg *runCfg, args []string, io commands.IO) error { + if len(args) != 2 { + return flag.ErrHelp + } + if cfg.rootCfg.gasWanted == 0 { + return errors.New("gas-wanted not specified") + } + if cfg.rootCfg.gasFee == "" { + return errors.New("gas-fee not specified") + } + + nameOrBech32 := args[0] + sourcePath := args[1] // can be a file path, a dir path, or '-' for stdin + + // read account pubkey. + kb, err := keys.NewKeyBaseFromDir(cfg.rootCfg.rootCfg.Home) + if err != nil { + return err + } + info, err := kb.GetByNameOrAddress(nameOrBech32) + if err != nil { + return err + } + caller := info.GetAddress() + + // parse gas wanted & fee. + gaswanted := cfg.rootCfg.gasWanted + gasfee, err := std.ParseCoin(cfg.rootCfg.gasFee) + if err != nil { + return errors.Wrap(err, "parsing gas fee coin") + } + + memPkg := &std.MemPackage{} + if sourcePath == "-" { // stdin + data, err := ioutil.ReadAll(io.In()) + if err != nil { + return fmt.Errorf("could not read stdin: %w", err) + } + memPkg.Files = []*std.MemFile{ + { + Name: "stdin.gno", + Body: string(data), + }, + } + } else { + info, err := os.Stat(sourcePath) + if err != nil { + return fmt.Errorf("could not read source path: %q, %w", sourcePath, err) + } + if info.IsDir() { + memPkg = gno.ReadMemPackage(sourcePath, "") + } else { // is file + b, err := os.ReadFile(sourcePath) + if err != nil { + return fmt.Errorf("could not read %q: %w", sourcePath, err) + } + memPkg.Files = []*std.MemFile{ + { + Name: info.Name(), + Body: string(b), + }, + } + } + } + if memPkg.IsEmpty() { + panic(fmt.Sprintf("found an empty package %q", memPkg.Path)) + } + // precompile and validate syntax + err = gno.PrecompileAndCheckMempkg(memPkg) + if err != nil { + panic(err) + } + memPkg.Name = "main" + memPkg.Path = "gno.land/r/" + caller.String() + "/run" + + // construct msg & tx and marshal. + msg := vm.MsgRun{ + Caller: caller, + Package: memPkg, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: cfg.rootCfg.memo, + } + + if cfg.rootCfg.broadcast { + err := signAndBroadcast(cfg.rootCfg, args, tx, io) + if err != nil { + return err + } + } else { + fmt.Println(string(amino.MustMarshalJSON(tx))) + } + return nil +}