Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

dev/moul/sourceless closure #1271

Draft
wants to merge 23 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gno.land/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ build.gnofaucet:; go build -o build/gnofaucet ./cmd/gnofaucet
build.gnokey:; go build -o build/gnokey ./cmd/gnokey
build.gnotxsync:; go build -o build/gnotxsync ./cmd/gnotxsync

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

Expand Down
11 changes: 4 additions & 7 deletions gno.land/cmd/gnoland/testdata/addpkg.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@ gnokey maketx addpkg -pkgdir $WORK -pkgpath gno.land/r/foobar/bar -gas-fee 10000
gnokey maketx call -pkgpath gno.land/r/foobar/bar -func Render -gas-fee 1000000ugnot -gas-wanted 2000000 -args '' -broadcast -chainid=tendermint_test test1

## compare render
cmp stdout stdout.golden
stdout '("hello from foo" string)'
stdout 'OK!'
stdout 'GAS WANTED: 2000000'
stdout 'GAS USED: '

-- bar.gno --
package bar

func Render(path string) string {
return "hello from foo"
}

-- stdout.golden --
("hello from foo" string)
OK!
GAS WANTED: 2000000
GAS USED: 69163
28 changes: 28 additions & 0 deletions gno.land/cmd/gnoland/testdata/run.txtar
Original file line number Diff line number Diff line change
@@ -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(""), "---")
}
43 changes: 43 additions & 0 deletions gno.land/cmd/gnoland/testdata/sourceless-closure.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## This example demonstrates how to store a closure, represented as a variable with its state and logic, in a contract. It also showcases the ability to upgrade the implementation of a closure between versions 1 and 2 of a realm while maintaining compatibility with the stored implementation in version 1.

gnoland start

gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1

gnokey maketx call -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -pkgpath gno.land/r/foo -func RunClosure test1
stdout 'aaa'

gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 $WORK/script/script.gno

#gnokey maketx call -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -pkgpath gno.land/r/foo -func RunClosure test1
#stderr 'package value missing in store: gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/run'

gnokey maketx addpkg -pkgdir $WORK/dummy -pkgpath gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/run -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1

#gnokey maketx call -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -pkgpath gno.land/r/foo -func RunClosure test1
#stderr 'package value missing in store: gno.land/r/g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5/run'

#gnokey maketx run -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test test1 $WORK/script/script.gno

gnokey maketx call -gas-fee 1000000ugnot -gas-wanted 2000000 -broadcast -chainid=tendermint_test -pkgpath gno.land/r/foo -func RunClosure test1
stdout 'bbb42'

-- foo/foo.gno --
package foo
func init() { clos = func() string {return "aaa"} }
var clos func() string
func SetClosure(c func() string) {clos = c}
func RunClosure() string {return clos()}

-- script/script.gno --
package main
import "gno.land/r/foo"
import "strconv"
func stuff() string { return "stuff" }
func main() {
var i = 42
foo.SetClosure(func() string { return "bbb" + strconv.Itoa(i) + stuff() })
}

-- dummy/dummy.gno --
package dummy
21 changes: 21 additions & 0 deletions gno.land/pkg/sdk/vm/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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

Expand Down
85 changes: 85 additions & 0 deletions gno.land/pkg/sdk/vm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package vm
// TODO: move most of the logic in ROOT/gno.land/...

import (
"bytes"
"fmt"
"os"
"strings"
Expand All @@ -27,6 +28,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{}
Expand Down Expand Up @@ -128,6 +130,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
Expand All @@ -150,6 +156,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)
}

// XXX: if ok, _ := regexp.MatchString(reReservedPath, pkgPath); ok {
// XXX: return ErrInvalidPkgPath("reserved package name: " + pkgPath)
// XXX: }

// Pay deposit from creator.
pkgAddr := gno.DerivePkgAddr(pkgPath)

Expand Down Expand Up @@ -282,6 +293,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)
Expand Down
58 changes: 58 additions & 0 deletions gno.land/pkg/sdk/vm/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
64 changes: 64 additions & 0 deletions gno.land/pkg/sdk/vm/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions gno.land/pkg/sdk/vm/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading