diff --git a/examples/gno.land/p/demo/nestedpkg/gno.mod b/examples/gno.land/p/demo/nestedpkg/gno.mod new file mode 100644 index 00000000000..24e16fdeb74 --- /dev/null +++ b/examples/gno.land/p/demo/nestedpkg/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/nestedpkg diff --git a/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno b/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno new file mode 100644 index 00000000000..4c489f430f9 --- /dev/null +++ b/examples/gno.land/p/demo/nestedpkg/nestedpkg.gno @@ -0,0 +1,89 @@ +// Package nestedpkg provides helpers for package-path based access control. +// It is useful for upgrade patterns relying on namespaces. +package nestedpkg + +// To test this from a realm and have std.CurrentRealm/PrevRealm work correctly, +// this file is tested from gno.land/r/demo/tests/nestedpkg_test.gno +// XXX: move test to ths directory once we support testing a package and +// specifying values for both PrevRealm and CurrentRealm. + +import ( + "std" + "strings" +) + +// IsCallerSubPath checks if the caller realm is located in a subfolder of the current realm. +func IsCallerSubPath() bool { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + return strings.HasPrefix(prev, cur) +} + +// AssertCallerIsSubPath panics if IsCallerSubPath returns false. +func AssertCallerIsSubPath() { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + if !strings.HasPrefix(prev, cur) { + panic("call restricted to nested packages. current realm is " + cur + ", previous realm is " + prev) + } +} + +// IsCallerParentPath checks if the caller realm is located in a parent location of the current realm. +func IsCallerParentPath() bool { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + return strings.HasPrefix(cur, prev) +} + +// AssertCallerIsParentPath panics if IsCallerParentPath returns false. +func AssertCallerIsParentPath() { + var ( + cur = std.CurrentRealm().PkgPath() + "/" + prev = std.PrevRealm().PkgPath() + "/" + ) + if !strings.HasPrefix(cur, prev) { + panic("call restricted to parent packages. current realm is " + cur + ", previous realm is " + prev) + } +} + +// IsSameNamespace checks if the caller realm and the current realm are in the same namespace. +func IsSameNamespace() bool { + var ( + cur = nsFromPath(std.CurrentRealm().PkgPath()) + "/" + prev = nsFromPath(std.PrevRealm().PkgPath()) + "/" + ) + return cur == prev +} + +// AssertIsSameNamespace panics if IsSameNamespace returns false. +func AssertIsSameNamespace() { + var ( + cur = nsFromPath(std.CurrentRealm().PkgPath()) + "/" + prev = nsFromPath(std.PrevRealm().PkgPath()) + "/" + ) + if cur != prev { + panic("call restricted to packages from the same namespace. current realm is " + cur + ", previous realm is " + prev) + } +} + +// nsFromPath extracts the namespace from a package path. +func nsFromPath(pkgpath string) string { + parts := strings.Split(pkgpath, "/") + + // Specifically for gno.land, potential paths are in the form of DOMAIN/r/NAMESPACE/... + // XXX: Consider extra checks. + // XXX: Support non gno.land domains, where p/ and r/ won't be enforced. + if len(parts) >= 3 { + return parts[2] + } + return "" +} + +// XXX: Consider adding IsCallerDirectlySubPath +// XXX: Consider adding IsCallerDirectlyParentPath diff --git a/examples/gno.land/r/demo/tests/gno.mod b/examples/gno.land/r/demo/tests/gno.mod index f34d41d327a..c51571e7d04 100644 --- a/examples/gno.land/r/demo/tests/gno.mod +++ b/examples/gno.land/r/demo/tests/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/demo/tests -require gno.land/r/demo/tests/subtests v0.0.0-latest +require ( + gno.land/p/demo/nestedpkg v0.0.0-latest + gno.land/r/demo/tests/subtests v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/tests/nestedpkg_test.gno b/examples/gno.land/r/demo/tests/nestedpkg_test.gno new file mode 100644 index 00000000000..904e8cc71a7 --- /dev/null +++ b/examples/gno.land/r/demo/tests/nestedpkg_test.gno @@ -0,0 +1,73 @@ +package tests + +import ( + "std" + "testing" +) + +func TestNestedPkg(t *testing.T) { + // direct child + cur := "gno.land/r/demo/tests/foo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if !IsCallerSubPath() { + t.Errorf(cur + " should be a sub path") + } + if IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if !HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // grand-grand-child + cur = "gno.land/r/demo/tests/foo/bar/baz" + std.TestSetRealm(std.NewCodeRealm(cur)) + if !IsCallerSubPath() { + t.Errorf(cur + " should be a sub path") + } + if IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if !HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // direct parent + cur = "gno.land/r/demo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if !IsCallerParentPath() { + t.Errorf(cur + " should be a parent path") + } + if !HasCallerSameNamespace() { + t.Errorf(cur + " should be from the same namespace") + } + + // fake parent (prefix) + cur = "gno.land/r/dem" + std.TestSetRealm(std.NewCodeRealm(cur)) + if IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if HasCallerSameNamespace() { + t.Errorf(cur + " should not be from the same namespace") + } + + // different namespace + cur = "gno.land/r/foo" + std.TestSetRealm(std.NewCodeRealm(cur)) + if IsCallerSubPath() { + t.Errorf(cur + " should not be a sub path") + } + if IsCallerParentPath() { + t.Errorf(cur + " should not be a parent path") + } + if HasCallerSameNamespace() { + t.Errorf(cur + " should not be from the same namespace") + } +} diff --git a/examples/gno.land/r/demo/tests/tests.gno b/examples/gno.land/r/demo/tests/tests.gno index 773412c3db9..421ac6528c9 100644 --- a/examples/gno.land/r/demo/tests/tests.gno +++ b/examples/gno.land/r/demo/tests/tests.gno @@ -3,6 +3,7 @@ package tests import ( "std" + "gno.land/p/demo/nestedpkg" rsubtests "gno.land/r/demo/tests/subtests" ) @@ -99,3 +100,15 @@ func GetRSubtestsPrevRealm() std.Realm { func Exec(fn func()) { fn() } + +func IsCallerSubPath() bool { + return nestedpkg.IsCallerSubPath() +} + +func IsCallerParentPath() bool { + return nestedpkg.IsCallerParentPath() +} + +func HasCallerSameNamespace() bool { + return nestedpkg.IsSameNamespace() +} diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/README.md b/examples/gno.land/r/x/manfred_upgrade_patterns/README.md index 4b85cf6b230..8af19beb273 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/README.md +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/README.md @@ -1 +1,40 @@ -Various upgrade pattern explorations. +# Various upgrade pattern explorations + +This repository explores different upgrade patterns for Gno smart contracts. + +## `upgrade_a` + +- Versions are independent. +- Versions are not pausable; users can interact with them independently. +- New versions wrap the previous one (can be recursive) to extend the logic and optionally the storage. +- There is no consistency between versions; updating a version will impact the more recent ones but won't affect the older ones. +- Users and contracts interacting with non-latest versions won't have the latest state. + +## `upgrade_b` + +- Versions include a `SetNextVersion` function which pauses the current implementation and invites users interacting with a deprecated version to switch to the most recent one. +- Since only one version can be used at a time, the latest version can safely recycle the previous version's state in read-only mode. +- These logics can be applied recursively. +- Users and contracts interacting with non-latest versions will switch to a more restricted version (read-only). + +## `upgrade_c` + +- `root` is the storage contract with simple logic. +- Versions implement the logic and rely on `root` to manage the state. +- In the current example, only one version can write to `root` (the latest); in practice, it could be possible to support various logics concurrently relying on `root` for storage. + +## `upgrade_d` -- "Lazy Migration" + +- Demonstrates lazy migrations from v1 to v2 of a data structure in Gno. +- Uses AVL trees, but storage can vary since public `Get` functions are used. +- v1 can be made pausable and read-only during migration. + +## `upgrade_e` + +- `home` is the front-facing contract, focusing on exposing a consistent API to users. +- Versions implement an interface that `home` looks for and self-register themselves, which instantly makes `home` use the new logic implementation for ongoing calls. + +## `upgrade_f` + +- Similar to `upgrade_e`. +- Replaces self-registration with manual registration by an admin. diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_test.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_filetest.gno similarity index 97% rename from examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_test.gno rename to examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_filetest.gno index 491bc6575bf..31130ce8282 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_test.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_a/integration_filetest.gno @@ -1,4 +1,4 @@ -package upgradea +package main import ( v1 "gno.land/r/x/manfred_upgrade_patterns/upgrade_a/v1" diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/integration_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/integration_filetest.gno new file mode 100644 index 00000000000..54bd32c194a --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/integration_filetest.gno @@ -0,0 +1,53 @@ +package main + +import ( + "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root" + "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1" + "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2" +) + +func main() { + println("# v1 impl") + println("root.Get()", root.Get()) + println("v1.Get()", v1.Get()) + println("v1.Inc()", v1.Inc()) + println("v1.Inc()", v1.Inc()) + println("v1.Inc()", v1.Inc()) + println("v1.Get()", v1.Get()) + println() + + println("# v2 impl") + root.SetCurrentImpl("gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2") + println("v2.Get()", v2.Get()) + println("v2.Inc()", v2.Inc()) + println("v2.Inc()", v2.Inc()) + println("v2.Inc()", v2.Inc()) + println("v2.Get()", v2.Get()) + println() + + println("# getters") + println("root.Get()", root.Get()) + println("v1.Get()", v1.Get()) + println("v2.Get()", v2.Get()) +} + +// Output: +// # v1 impl +// root.Get() 0 +// v1.Get() 0 +// v1.Inc() 1 +// v1.Inc() 2 +// v1.Inc() 3 +// v1.Get() 3 +// +// # v2 impl +// v2.Get() 6 +// v2.Inc() 1003 +// v2.Inc() 2003 +// v2.Inc() 3003 +// v2.Get() 6006 +// +// # getters +// root.Get() 3003 +// v1.Get() 3003 +// v2.Get() 6006 diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root/root.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root/root.gno index 926b347c1bf..0a610b0b196 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root/root.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root/root.gno @@ -1,13 +1,15 @@ package root +import "std" + var ( - counter int - currentImplementation = "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1" + counter int + currentImpl = "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1" ) -func Inc() int { - // TODO: if caller is currentImplementation - counter++ +func Inc(nb int) int { + assertIsCurrentImpl() + counter += nb return counter } @@ -15,7 +17,17 @@ func Get() int { return counter } -func UpdateCurrentImplementation(address string) { - // TODO: if is admin - currentImplementation = address +func SetCurrentImpl(pkgpath string) { + assertIsAdmin() + currentImpl = pkgpath +} + +func assertIsCurrentImpl() { + if std.PrevRealm().PkgPath() != currentImpl { + panic("unauthorized") + } +} + +func assertIsAdmin() { + // TODO } diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1/v1.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1/v1.gno index 498217dfba0..d994f36a277 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1/v1.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v1/v1.gno @@ -2,8 +2,8 @@ package v1 import "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root" -func Inc() { - root.Inc() +func Inc() int { + return root.Inc(1) } func Get() int { diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2/v2.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2/v2.gno index 03ffe876519..11e6c4e5602 100644 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2/v2.gno +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_c/v2/v2.gno @@ -2,8 +2,8 @@ package v2 import "gno.land/r/x/manfred_upgrade_patterns/upgrade_c/root" -func Inc() { - root.Inc() +func Inc() int { + return root.Inc(1000) } func Get() int { diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/README.md b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/README.md deleted file mode 100644 index fa73113ef3b..00000000000 --- a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_d/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Lazy Migration Example - -This example demonstrates lazy migrations from v1 to v2 of a data structure in Gno. - -## Notes - -Uses AVL trees, but storage can vary since public Get functions are used. - -v1 can be made pausable and readonly during migration. diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/home.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/home.gno new file mode 100644 index 00000000000..418bc59236a --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/home.gno @@ -0,0 +1,38 @@ +package home + +import "gno.land/p/demo/nestedpkg" + +type myInterface interface { + Render(path string) string + Foo() error +} + +var currentImpl myInterface + +func SetImpl(impl myInterface) { + nestedpkg.AssertCallerIsSubPath() + currentImpl = impl +} + +func Render(path string) string { + assertImplIsDefined() + return currentImpl.Render(path) +} + +func Foo() error { + assertImplIsDefined() + return currentImpl.Foo() +} + +func Bar() error { + // doing some extra logic here + err := currentImpl.Foo() + // doing some more extra logic here + return err +} + +func assertImplIsDefined() { + if currentImpl == nil { + panic("no implementation") + } +} diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/v1impl.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/v1impl.gno new file mode 100644 index 00000000000..7ca2b1d7900 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/v1impl.gno @@ -0,0 +1,19 @@ +package v1impl + +import ( + "errors" + + home "gno.land/r/x/manfred_upgrade_patterns/upgrade_e" +) + +// init is for self-registration, but in practice, anything can register like a `maketx run` call by an admin. +func init() { + // self register on init + impl := &Impl{} + home.SetImpl(impl) +} + +type Impl struct{} + +func (i Impl) Render(path string) string { return "hello from v1" } +func (i Impl) Foo() error { return errors.New("not implemented") } diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/z_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/z_filetest.gno new file mode 100644 index 00000000000..cd93c44a7ac --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl/z_filetest.gno @@ -0,0 +1,15 @@ +package main + +import ( + home "gno.land/r/x/manfred_upgrade_patterns/upgrade_e" + _ "gno.land/r/x/manfred_upgrade_patterns/upgrade_e/v1impl" +) + +func main() { + println(home.Render("")) + println(home.Foo()) +} + +// Output: +// hello from v1 +// not implemented diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/home/home.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/home/home.gno new file mode 100644 index 00000000000..bee0fadb3b2 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/home/home.gno @@ -0,0 +1,40 @@ +package home + +type myInterface interface { + Render(path string) string + Foo() error +} + +var currentImpl myInterface + +func SetImpl(impl myInterface) { + assertIsAdmin() + currentImpl = impl +} + +func Render(path string) string { + assertImplIsDefined() + return currentImpl.Render(path) +} + +func Foo() error { + assertImplIsDefined() + return currentImpl.Foo() +} + +func Bar() error { + // doing some extra logic here + err := currentImpl.Foo() + // doing some more extra logic here + return err +} + +func assertImplIsDefined() { + if currentImpl == nil { + panic("no implementation") + } +} + +func assertIsAdmin() { + // TODO: unsafe +} diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/v1impl.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/v1impl.gno new file mode 100644 index 00000000000..2a1dc0715c0 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/v1impl.gno @@ -0,0 +1,14 @@ +package v1impl + +import "errors" + +var impl = &Impl{} + +func Instance() *Impl { + return impl +} + +type Impl struct{} + +func (i Impl) Render(path string) string { return "hello from v1" } +func (i Impl) Foo() error { return errors.New("not implemented") } diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/z_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/z_filetest.gno new file mode 100644 index 00000000000..685625d92a3 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl/z_filetest.gno @@ -0,0 +1,16 @@ +package main + +import ( + "gno.land/r/x/manfred_upgrade_patterns/upgrade_f/home" + "gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl" +) + +func main() { + home.SetImpl(v1impl.Instance()) + println(home.Render("")) + println(home.Foo()) +} + +// Output: +// hello from v1 +// not implemented diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/v2impl.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/v2impl.gno new file mode 100644 index 00000000000..66864392715 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/v2impl.gno @@ -0,0 +1,12 @@ +package v2impl + +var impl = &Impl{} + +func Instance() *Impl { + return impl +} + +type Impl struct{} + +func (i Impl) Render(path string) string { return "hello from v2" } +func (i Impl) Foo() error { return nil } diff --git a/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/z_filetest.gno b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/z_filetest.gno new file mode 100644 index 00000000000..5f7d78c39a3 --- /dev/null +++ b/examples/gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl/z_filetest.gno @@ -0,0 +1,25 @@ +package main + +import ( + "gno.land/r/x/manfred_upgrade_patterns/upgrade_f/home" + "gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v1impl" + "gno.land/r/x/manfred_upgrade_patterns/upgrade_f/v2impl" +) + +func main() { + home.SetImpl(v1impl.Instance()) + println(home.Render("")) + println(home.Foo()) + + println("-------------") + home.SetImpl(v2impl.Instance()) + println(home.Render("")) + println(home.Foo()) +} + +// Output: +// hello from v1 +// not implemented +// ------------- +// hello from v2 +// undefined