diff --git a/contribs/Makefile b/contribs/Makefile index bfd1fcb9889..04b607cb32d 100644 --- a/contribs/Makefile +++ b/contribs/Makefile @@ -3,6 +3,8 @@ help: @echo "Available make commands:" @cat Makefile | grep '^[a-z][^:]*:' | cut -d: -f1 | sort | sed 's/^/ /' +programs=gnodev gnofaucet + # command to run dependency utilities, like goimports. rundep=go run -modfile ../misc/devdeps/go.mod @@ -25,15 +27,31 @@ GOTEST_FLAGS ?= -v -p 1 -timeout=30m ######################################## # Dev tools .PHONY: install -install: install.gnomd install.gnodev install.gnofaucet +install: + @echo 'To install a tool, go to the subdirectory, then run `make install`.' + @echo 'To do a full installation, run `make install_all`.' + +install_all: $(addsuffix .install,$(programs)) +%.install: + @echo "[+] make -C $(basename $@) install" + $(MAKE) --no-print-directory -C $(basename $@) install +.PHONY: install_all -install.gnomd:; cd gnomd && go install . -install.gnodev:; $(MAKE) -C ./gnodev install -install.gnofaucet:; $(MAKE) -C ./gnofaucet install +######################################## +# Test suite +test: $(addsuffix .test,$(programs)) +%.test: + @echo "[+] make -C $(basename $@) install" + $(MAKE) --no-print-directory -C $(basename $@) test +.PHONY: test -.PHONY: clean -clean: - rm -rf build +######################################## +# Lint +.PHONY: lint +lint: $(addsuffix .lint,$(programs)) +%.lint: + @echo "[+] make -C $(basename $@) install" + $(MAKE) --no-print-directory -C $(basename $@) lint ######################################## # Dev tools @@ -45,23 +63,4 @@ fmt: .PHONY: tidy tidy: - @for gomod in `find . -name go.mod`; do ( \ - dir=`dirname $$gomod`; \ - set -xe; \ - cd $$dir; \ - go mod tidy -v; \ - ); done - -######################################## -# Test suite -.PHONY: test -test: test.gnodev -test.gnodev: - $(MAKE) -C ./gnodev test - -######################################## -# Lint -.PHONY: test -lint: lint.gnodev -lint.gnodev: - $(MAKE) -C ./gnodev test + find . -name go.mod -execdir go mod tidy -v \; diff --git a/examples/Makefile b/examples/Makefile index 39a51a32112..4894e28a1bb 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -27,6 +27,7 @@ OFFICIAL_PACKAGES = ./gno.land/p OFFICIAL_PACKAGES += ./gno.land/r/demo OFFICIAL_PACKAGES += ./gno.land/r/gnoland OFFICIAL_PACKAGES += ./gno.land/r/sys +OFFICIAL_PACKAGES += ./gno.land/r/gov ######################################## # Dev tools diff --git a/examples/gno.land/p/demo/ownable/ownable.gno b/examples/gno.land/p/demo/ownable/ownable.gno index 7f2eac008e1..22c0c33acc6 100644 --- a/examples/gno.land/p/demo/ownable/ownable.gno +++ b/examples/gno.land/p/demo/ownable/ownable.gno @@ -12,10 +12,14 @@ type Ownable struct { func New() *Ownable { return &Ownable{ - owner: std.GetOrigCaller(), + owner: std.PrevRealm().Addr(), } } +func NewWithAddress(addr std.Address) *Ownable { + return &Ownable{owner: addr} +} + // TransferOwnership transfers ownership of the Ownable struct to a new address func (o *Ownable) TransferOwnership(newOwner std.Address) error { err := o.CallerIsOwner() @@ -44,14 +48,20 @@ func (o *Ownable) DropOwnership() error { return nil } +func (o Ownable) Owner() std.Address { + return o.owner +} + // CallerIsOwner checks if the caller of the function is the Realm's owner -func (o *Ownable) CallerIsOwner() error { - if std.GetOrigCaller() == o.owner { +func (o Ownable) CallerIsOwner() error { + if std.PrevRealm().Addr() == o.owner { return nil } return ErrUnauthorized } -func (o *Ownable) Owner() std.Address { - return o.owner +func (o Ownable) AssertCallerIsOwner() { + if std.PrevRealm().Addr() != o.owner { + panic(ErrUnauthorized) + } } diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index f725795fd47..2c28f867f4a 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -11,28 +11,39 @@ var ( ) func TestNew(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed - result := New() - if firstCaller != result.owner { - t.Fatalf("Expected %s, got: %s\n", firstCaller, result.owner) + o := New() + got := o.Owner() + if firstCaller != got { + t.Fatalf("Expected %s, got: %s", firstCaller, got) } } -func TestOwner(t *testing.T) { - std.TestSetOrigCaller(firstCaller) +func TestNewWithAddress(t *testing.T) { + o := NewWithAddress(firstCaller) + + got := o.Owner() + if firstCaller != got { + t.Fatalf("Expected %s, got: %s", firstCaller, got) + } +} - result := New() - resultOwner := result.Owner() +func TestOwner(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(firstCaller)) + o := New() expected := firstCaller - if resultOwner != expected { - t.Fatalf("Expected %s, got: %s\n", expected, result) + got := o.Owner() + if expected != got { + t.Fatalf("Expected %s, got: %s", expected, got) } } func TestTransferOwnership(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) + o := New() err := o.TransferOwnership(secondCaller) @@ -40,28 +51,29 @@ func TestTransferOwnership(t *testing.T) { t.Fatalf("TransferOwnership failed, %v", err) } - result := o.Owner() - if secondCaller != result { - t.Fatalf("Expected: %s, got: %s\n", secondCaller, result) + got := o.Owner() + if secondCaller != got { + t.Fatalf("Expected: %s, got: %s", secondCaller, got) } } func TestCallerIsOwner(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) o := New() unauthorizedCaller := secondCaller - std.TestSetOrigCaller(unauthorizedCaller) + std.TestSetRealm(std.NewUserRealm(unauthorizedCaller)) + std.TestSetOrigCaller(unauthorizedCaller) // TODO(bug): should not be needed err := o.CallerIsOwner() if err == nil { - t.Fatalf("Expected %s to not be owner\n", unauthorizedCaller) + t.Fatalf("Expected %s to not be owner", unauthorizedCaller) } } func TestDropOwnership(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) o := New() @@ -72,18 +84,20 @@ func TestDropOwnership(t *testing.T) { owner := o.Owner() if owner != "" { - t.Fatalf("Expected owner to be empty, not %s\n", owner) + t.Fatalf("Expected owner to be empty, not %s", owner) } } // Errors func TestErrUnauthorized(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) + std.TestSetOrigCaller(firstCaller) // TODO(bug): should not be needed o := New() - std.TestSetOrigCaller(secondCaller) + std.TestSetRealm(std.NewUserRealm(secondCaller)) + std.TestSetOrigCaller(secondCaller) // TODO(bug): should not be needed err := o.TransferOwnership(firstCaller) if err != ErrUnauthorized { @@ -97,7 +111,7 @@ func TestErrUnauthorized(t *testing.T) { } func TestErrInvalidAddress(t *testing.T) { - std.TestSetOrigCaller(firstCaller) + std.TestSetRealm(std.NewUserRealm(firstCaller)) o := New() diff --git a/examples/gno.land/p/gov/proposal/gno.mod b/examples/gno.land/p/gov/proposal/gno.mod new file mode 100644 index 00000000000..41ab9d196f1 --- /dev/null +++ b/examples/gno.land/p/gov/proposal/gno.mod @@ -0,0 +1 @@ +module proposal diff --git a/examples/gno.land/p/gov/proposal/proposal.gno b/examples/gno.land/p/gov/proposal/proposal.gno new file mode 100644 index 00000000000..f01d3661a5e --- /dev/null +++ b/examples/gno.land/p/gov/proposal/proposal.gno @@ -0,0 +1,39 @@ +// Package proposal provides a structure for executing proposals. +package proposal + +// NewExecutor creates a new executor with the provided callback function. +func NewExecutor(callback func() error) Executor { + return &executorImpl{ + callback: callback, + done: false, + } +} + +// executorImpl is an implementation of the Executor interface. +type executorImpl struct { + callback func() error + done bool + success bool +} + +// execute runs the executor's callback function. +func (exec *executorImpl) Execute() error { + if exec.done { + return ErrAlreadyDone + } + // XXX: assertCalledByGovdao + err := exec.callback() + exec.done = true + exec.success = err == nil + return err +} + +// Done returns whether the executor has been executed. +func (exec *executorImpl) Done() bool { + return exec.done +} + +// Success returns whether the execution was successful. +func (exec *executorImpl) Success() bool { + return exec.success +} diff --git a/examples/gno.land/p/gov/proposal/types.gno b/examples/gno.land/p/gov/proposal/types.gno new file mode 100644 index 00000000000..a2ebad6f585 --- /dev/null +++ b/examples/gno.land/p/gov/proposal/types.gno @@ -0,0 +1,16 @@ +// Package proposal defines types for proposal execution. +package proposal + +import "errors" + +// Executor represents a minimal closure-oriented proposal design. +// It is intended to be used by a govdao governance proposal (v1, v2, etc). +type Executor interface { + Execute() error + Done() bool + Success() bool // Done() && !err +} + +// ErrAlreadyDone is the error returned when trying to execute an already +// executed proposal. +var ErrAlreadyDone = errors.New("already executed") diff --git a/examples/gno.land/r/gnoland/valopers/gno.mod b/examples/gno.land/r/gnoland/valopers/gno.mod new file mode 100644 index 00000000000..ae4207cd919 --- /dev/null +++ b/examples/gno.land/r/gnoland/valopers/gno.mod @@ -0,0 +1,6 @@ +module valopers + +require ( + gno.land/p/demo/ownable v0.0.0-latest + gno.land/r/gov/dao v0.0.0-latest +) diff --git a/examples/gno.land/r/gnoland/valopers/valopers.gno b/examples/gno.land/r/gnoland/valopers/valopers.gno new file mode 100644 index 00000000000..e35eb749c8f --- /dev/null +++ b/examples/gno.land/r/gnoland/valopers/valopers.gno @@ -0,0 +1,39 @@ +// Package valopers is designed around the permissionless lifecycle of valoper profiles. +// It also includes parts designed for govdao to propose valset changes based on registered valopers. +package valopers + +import ( + "std" + + "gno.land/p/demo/ownable" + govdao "gno.land/r/gov/dao" +) + +// Valoper represents a validator operator profile. +type Valoper struct { + ownable.Ownable // Embedding the Ownable type for ownership management. + + DisplayName string // The display name of the valoper. + ValidatorAddr std.Address // The address of the validator. + // TODO: Add other valoper metadata as needed. +} + +// Register registers a new valoper. +// TODO: Define the parameters and implement the function. +func Register( /* TBD */ ) { + panic("not implemented") +} + +// Update updates an existing valoper. +// TODO: Define the parameters and implement the function. +func Update( /* TBD */ ) { + panic("not implemented") +} + +// GovXXX is a placeholder for a function to interact with the governance DAO. +// TODO: Define a good API and implement it. +func GovXXX() { + // Assert that the caller is a member of the governance DAO. + govdao.AssertIsMember(std.PrevRealm().Addr()) + panic("not implemented") +} diff --git a/examples/gno.land/r/gov/dao/dao.gno b/examples/gno.land/r/gov/dao/dao.gno new file mode 100644 index 00000000000..37caca9fa19 --- /dev/null +++ b/examples/gno.land/r/gov/dao/dao.gno @@ -0,0 +1,65 @@ +package govdao + +import ( + "std" + + "gno.land/p/gov/proposal" +) + +var proposals = make([]Proposal, 0) + +// XXX var members ... + +// Proposal represents a proposal in the governance system. +type Proposal struct { + author std.Address + idx int + Comment string + Executor proposal.Executor +} + +// Propose is designed to be called by another contract or with +// `maketx run`, not by a `maketx call`. +func Propose(proposal Proposal) int { + // XXX: require payment? + // XXX: sanitize proposal + caller := std.PrevRealm().Addr() + AssertIsMember(caller) + proposal.author = caller + proposal.idx = len(proposals) + proposals = append(proposals, proposal) + return proposal.idx +} + +func VoteOnProposal(idx int, option string) { + caller := std.PrevRealm().Addr() + AssertIsMember(caller) + panic("not implemented") + // XXX: implement the voting (woudl be cool to have a generic p/) +} + +func ExecuteProposal(idx int) { + assertProposalExists(idx) + // XXX: assert voting is finished + // XXX: assert voting result is YES + // XXX: proposal was not already executed + proposal := proposals[idx] + proposal.Executor.Execute() +} + +func assertProposalExists(idx int) { + if idx < 0 || idx >= len(proposals) { + panic("invalid proposal id") + } +} + +func IsMember(addr std.Address) bool { + // XXX: implement + return true +} + +func AssertIsMember(addr std.Address) { + if !IsMember(addr) { + panic("caller is not member of govdao") + } +} diff --git a/examples/gno.land/r/gov/dao/gno.mod b/examples/gno.land/r/gov/dao/gno.mod new file mode 100644 index 00000000000..61bfabc18d0 --- /dev/null +++ b/examples/gno.land/r/gov/dao/gno.mod @@ -0,0 +1,3 @@ +module govdao + +require gno.land/p/gov/proposal v0.0.0-latest diff --git a/examples/gno.land/r/gov/proposals/prop1/gno.mod b/examples/gno.land/r/gov/proposals/prop1/gno.mod new file mode 100644 index 00000000000..6a030fcba0a --- /dev/null +++ b/examples/gno.land/r/gov/proposals/prop1/gno.mod @@ -0,0 +1,6 @@ +module prop1 + +require ( + gno.land/r/gov/dao v0.0.0-latest + gno.land/r/sys/validators v0.0.0-latest +) diff --git a/examples/gno.land/r/gov/proposals/prop1/prop1.gno b/examples/gno.land/r/gov/proposals/prop1/prop1.gno new file mode 100644 index 00000000000..6e5edb0c97b --- /dev/null +++ b/examples/gno.land/r/gov/proposals/prop1/prop1.gno @@ -0,0 +1,39 @@ +// Package prop1 is an example of proposal creation using a contract. +// +// Please note that this package is intended for demonstration purposes only. +// You could execute this code by running a `maketx run` command or by uploading +// a similar package to a personal namespace. +// +// For the specific case of validators, a `r/gnoland/valopers` will be used to +// organize the lifecycle of validators (register, etc), and this more complex +// contract will be responsible to generate proposals. +package prop1 + +import ( + "std" + + govdao "gno.land/r/gov/dao" + "gno.land/r/sys/validators" +) + +func init() { + // Create the validators change proposal. + changesFn := func() []validators.Change { + return []validators.Change{ + {Address: std.Address("g12345678"), Power: 1}, // add a new validator + {Address: std.Address("g000000000"), Power: 0}, // remove an existing validator + } + } + + // Wraps changesFn to emit a certified event only if executed from a + // complete governance proposal process. + executor := validators.NewProposalExecutor(changesFn) + + // Create a proposal. + // XXX: payment + proposal := govdao.Proposal{ + Comment: "manual valset changes proposal example", + Executor: executor, + } + govdao.Propose(proposal) +} diff --git a/examples/gno.land/r/sys/validators/gno.mod b/examples/gno.land/r/sys/validators/gno.mod index 84df66b9001..51f6058a35a 100644 --- a/examples/gno.land/r/sys/validators/gno.mod +++ b/examples/gno.land/r/sys/validators/gno.mod @@ -1 +1,3 @@ module gno.land/r/sys/validators + +require gno.land/p/gov/proposal v0.0.0-latest diff --git a/examples/gno.land/r/sys/validators/validators.gno b/examples/gno.land/r/sys/validators/validators.gno index dc9d731f075..0e6e132f6e9 100644 --- a/examples/gno.land/r/sys/validators/validators.gno +++ b/examples/gno.land/r/sys/validators/validators.gno @@ -1,4 +1,47 @@ -// This package is used to manage the validator set. +// Package validators is used to manage the validator set. package validators -// write specs. +import ( + "std" + + "gno.land/p/gov/proposal" +) + +var unappliedChanges = []Change{} + +// Change represents a change in the validator set. +type Change struct { + Address std.Address + Power int +} + +// NewProposalExecutor creates a new executor that wraps a changes closure +// proposal. It emits a typed object (subscribed by tm2) only if it passes +// through a complete p/gov/proposal process. +func NewProposalExecutor(changesFn func() []Change) proposal.Executor { + if changesFn == nil { + panic("changesFn should not be nil") + } + + // Certify that the changes are sent from the context of this realm. + callback := func() error { + newChanges := changesFn() + + // emit for external clients + std.Emit("newChanges") // XXX: pass parameters + + // append to slice for gno.land + unappliedChanges = append(unappliedChanges, newChanges...) + return nil + } + + exec := proposal.NewExecutor(callback) + return exec +} + +// this function is unexported and intended to be called by the chain. +func getAndResetChanges() []Change { + cpy := unappliedChanges[:] + unappliedChanges = []Change{} + return cpy +} diff --git a/gnovm/pkg/gnomod/gnomod.go b/gnovm/pkg/gnomod/gnomod.go index af946f2bf39..014873b8faa 100644 --- a/gnovm/pkg/gnomod/gnomod.go +++ b/gnovm/pkg/gnomod/gnomod.go @@ -99,7 +99,7 @@ func GnoToGoMod(f File) (*File, error) { gnoModPath := GetGnoModPath() if strings.HasPrefix(f.Module.Mod.Path, transpiler.GnoRealmPkgsPrefixBefore) || - strings.HasPrefix(f.Module.Mod.Path, transpiler.GnoPackagePrefixBefore) { + strings.HasPrefix(f.Module.Mod.Path, transpiler.GnoPurePkgsPrefixBefore) { f.AddModuleStmt(transpiler.ImportPrefix + "/examples/" + f.Module.Mod.Path) } @@ -112,7 +112,7 @@ func GnoToGoMod(f File) (*File, error) { } path := f.Require[i].Mod.Path if strings.HasPrefix(f.Require[i].Mod.Path, transpiler.GnoRealmPkgsPrefixBefore) || - strings.HasPrefix(f.Require[i].Mod.Path, transpiler.GnoPackagePrefixBefore) { + strings.HasPrefix(f.Require[i].Mod.Path, transpiler.GnoPurePkgsPrefixBefore) { // Add dependency with a modified import path f.AddRequire(transpiler.ImportPrefix+"/examples/"+f.Require[i].Mod.Path, f.Require[i].Mod.Version) } diff --git a/gnovm/pkg/transpiler/transpiler.go b/gnovm/pkg/transpiler/transpiler.go index e3d817700d0..8a91ae4a486 100644 --- a/gnovm/pkg/transpiler/transpiler.go +++ b/gnovm/pkg/transpiler/transpiler.go @@ -22,8 +22,8 @@ import ( const ( GnoRealmPkgsPrefixBefore = "gno.land/r/" GnoRealmPkgsPrefixAfter = "github.com/gnolang/gno/examples/gno.land/r/" - GnoPackagePrefixBefore = "gno.land/p/demo/" - GnoPackagePrefixAfter = "github.com/gnolang/gno/examples/gno.land/p/demo/" + GnoPurePkgsPrefixBefore = "gno.land/p/" + GnoPurePkgsPrefixAfter = "github.com/gnolang/gno/examples/gno.land/p/" GnoStdPkgBefore = "std" GnoStdPkgAfter = "github.com/gnolang/gno/gnovm/stdlibs/stdshim" ) @@ -277,7 +277,7 @@ func transpileAST(fset *token.FileSet, f *ast.File, checkWhitelist bool) (ast.No continue } - if strings.HasPrefix(importPath, GnoPackagePrefixBefore) { + if strings.HasPrefix(importPath, GnoPurePkgsPrefixBefore) { continue } @@ -320,8 +320,8 @@ func transpileAST(fset *token.FileSet, f *ast.File, checkWhitelist bool) (ast.No } // p/pkg packages - if strings.HasPrefix(importPath, GnoPackagePrefixBefore) { - target := GnoPackagePrefixAfter + strings.TrimPrefix(importPath, GnoPackagePrefixBefore) + if strings.HasPrefix(importPath, GnoPurePkgsPrefixBefore) { + target := GnoPurePkgsPrefixAfter + strings.TrimPrefix(importPath, GnoPurePkgsPrefixBefore) if !astutil.RewriteImport(fset, f, importPath, target) { errs.Add(fset.Position(importSpec.Pos()), fmt.Sprintf("failed to replace the %q package with %q", importPath, target))