Skip to content

Commit

Permalink
feat(examples): add two new upgrade examples + refactor existing ones (
Browse files Browse the repository at this point in the history
…#2334)

Depends on #2342

cc @jeronimoalbi

---------

Signed-off-by: moul <[email protected]>
Co-authored-by: Jerónimo Albi <[email protected]>
Co-authored-by: Morgan <[email protected]>
  • Loading branch information
3 people authored Jul 24, 2024
1 parent 3eaf449 commit 8865638
Show file tree
Hide file tree
Showing 20 changed files with 477 additions and 24 deletions.
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/nestedpkg/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/nestedpkg
89 changes: 89 additions & 0 deletions examples/gno.land/p/demo/nestedpkg/nestedpkg.gno
Original file line number Diff line number Diff line change
@@ -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
5 changes: 4 additions & 1 deletion examples/gno.land/r/demo/tests/gno.mod
Original file line number Diff line number Diff line change
@@ -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
)
73 changes: 73 additions & 0 deletions examples/gno.land/r/demo/tests/nestedpkg_test.gno
Original file line number Diff line number Diff line change
@@ -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")
}
}
13 changes: 13 additions & 0 deletions examples/gno.land/r/demo/tests/tests.gno
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tests
import (
"std"

"gno.land/p/demo/nestedpkg"
rsubtests "gno.land/r/demo/tests/subtests"
)

Expand Down Expand Up @@ -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()
}
41 changes: 40 additions & 1 deletion examples/gno.land/r/x/manfred_upgrade_patterns/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package upgradea
package main

import (
v1 "gno.land/r/x/manfred_upgrade_patterns/upgrade_a/v1"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
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
}

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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

This file was deleted.

Loading

0 comments on commit 8865638

Please sign in to comment.