From a4b5c9cb70812ffb62451d41b6423992eb1e9805 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Mon, 24 Feb 2025 14:51:12 +0100 Subject: [PATCH 1/8] fix(gnovm): allow private struct fields to be set (#3814) Go reflect doesn't allow to set unexported struct fields. We use `unsafe` to override this limitation. Fixes #1155, fixes #3802 --- gnovm/pkg/gnolang/gonative.go | 8 +++++++- gnovm/tests/files/struct59.gno | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 gnovm/tests/files/struct59.gno diff --git a/gnovm/pkg/gnolang/gonative.go b/gnovm/pkg/gnolang/gonative.go index 2fbf34ed1b1..cd0e3b9ce9f 100644 --- a/gnovm/pkg/gnolang/gonative.go +++ b/gnovm/pkg/gnolang/gonative.go @@ -4,6 +4,7 @@ import ( "fmt" "math" "reflect" + "unsafe" "github.com/gnolang/gno/gnovm/pkg/gnolang/internal/softfloat" ) @@ -1133,7 +1134,12 @@ func gno2GoValue(tv *TypedValue, rv reflect.Value) (ret reflect.Value) { if ftv.IsUndefined() { continue } - gno2GoValue(ftv, rv.Field(i)) + fv := rv.Field(i) + if !fv.CanSet() { + // Normally private fields can not bet set via reflect. Override this limitation. + fv = reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem() + } + gno2GoValue(ftv, fv) } case *MapType: // If uninitialized map, return zero value. diff --git a/gnovm/tests/files/struct59.gno b/gnovm/tests/files/struct59.gno new file mode 100644 index 00000000000..6c26978a808 --- /dev/null +++ b/gnovm/tests/files/struct59.gno @@ -0,0 +1,15 @@ +package main + +import "fmt" + +type X struct { + v string +} + +func main() { + x := X{v: "a"} + fmt.Printf("test %#v\n", x) +} + +// Output: +// test struct { v string }{v:"a"} From 27fd12dffb2101a05788bcf29d08f933ee3e527f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:04:18 +0100 Subject: [PATCH 2/8] docs: archive test4 (#3810) ## Description Deprecates test4 in the docs. --------- Co-authored-by: Jeff Thompson --- docs/concepts/namespaces.md | 6 +++--- docs/concepts/testnets.md | 22 +++++++--------------- docs/reference/network-config.md | 1 - 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/docs/concepts/namespaces.md b/docs/concepts/namespaces.md index c7f03ec1f0a..7e16dd9c48b 100644 --- a/docs/concepts/namespaces.md +++ b/docs/concepts/namespaces.md @@ -9,7 +9,7 @@ similar to GitHub's user and organization model. :::warning Not enabled -This feature isn't enabled by default on the portal loop chain and is currently available only on test4.gno.land. +This feature isn't enabled by default on the Portal Loop chain and is currently available only on test5.gno.land. ::: @@ -56,7 +56,7 @@ $ gnokey maketx call -pkgpath gno.land/r/demo/users \ -func Register \ -gas-fee 1000000ugnot -gas-wanted 2000000 \ -broadcast \ - -chainid=test4 \ + -chainid=test5 \ -send=20000000ugnot \ -args '' \ -args 'patrick' \ @@ -86,6 +86,6 @@ $ gnokey maketx addpkg \ --gas-fee 1000000ugnot \ --gas-wanted 2000000 \ --broadcast \ - --chainid test4 \ + --chainid test5 \ test1 ``` diff --git a/docs/concepts/testnets.md b/docs/concepts/testnets.md index b5286eaec57..9769c8f873f 100644 --- a/docs/concepts/testnets.md +++ b/docs/concepts/testnets.md @@ -63,21 +63,6 @@ Test5 was launched in November 2024. - **Versioning strategy**: - Test5 is to be release-based, following releases of the Gno tech stack. -## Test4 - -Test4 is the first permanent multi-node testnet, launched in July 2024. - -- **Persistence of state:** - - State is fully persisted unless there are breaking changes in a new release, - where persistence partly depends on implementing a migration strategy -- **Timeliness of code:** - - Versioning mechanisms for packages & realms will be implemented for test4 -- **Intended purpose** - - Running a full node, testing validator coordination, deploying stable Gno - dApps, creating tools that require persisted state & transaction history -- **Versioning strategy**: - - Test4 is the first gno.land testnet to be release-based, following releases -of the Gno tech stack. ## Staging @@ -98,6 +83,13 @@ Staging is a testnet that is reset once every 60 minutes. These testnets are deprecated and currently serve as archives of previous progress. +## Test4 + +Test4 is the first permanent multi-node testnet. Archived data for test4 can be found [here](https://github.com/gnolang/tx-exports/tree/main/test4.gno.land). + +Launch date: July 10th 2024 +Release commit: [194903d](https://github.com/gnolang/gno/commit/194903db0350ace7d57910e6c34125d3aa9817da) + ### Test3 (archive) The third Gno testnet. Archived data for test3 can be found [here](https://github.com/gnolang/tx-exports/tree/main/test3.gno.land). diff --git a/docs/reference/network-config.md b/docs/reference/network-config.md index 45a56b772ae..1e50864372b 100644 --- a/docs/reference/network-config.md +++ b/docs/reference/network-config.md @@ -8,7 +8,6 @@ id: network-config |-------------|----------------------------------|---------------| | Portal Loop | https://rpc.gno.land:443 | `portal-loop` | | Test5 | https://rpc.test5.gno.land:443 | `test5` | -| Test4 | https://rpc.test4.gno.land:443 | `test4` | | Staging | https://rpc.staging.gno.land:443 | `staging` | ### WebSocket endpoints From 300dcfde8de225ddf8366220a0837ff821cbf355 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 24 Feb 2025 16:32:06 +0100 Subject: [PATCH 3/8] feat(sdk/vm): disallow calling internal realms (#3794) continuation of #3774, fixes #2190. --- gno.land/pkg/sdk/vm/msgs.go | 3 +++ .../pkg/sdk/vm/{msg_test.go => msgs_test.go} | 14 ++++++++++++++ gnovm/pkg/gnolang/helpers.go | 16 ++++++++++++++++ gnovm/pkg/gnolang/preprocess.go | 5 +---- 4 files changed, 34 insertions(+), 4 deletions(-) rename gno.land/pkg/sdk/vm/{msg_test.go => msgs_test.go} (95%) diff --git a/gno.land/pkg/sdk/vm/msgs.go b/gno.land/pkg/sdk/vm/msgs.go index 38f35ab7110..51d573837fc 100644 --- a/gno.land/pkg/sdk/vm/msgs.go +++ b/gno.land/pkg/sdk/vm/msgs.go @@ -120,6 +120,9 @@ func (msg MsgCall) ValidateBasic() error { if !gno.IsRealmPath(msg.PkgPath) { return ErrInvalidPkgPath("pkgpath must be of a realm") } + if _, isInt := gno.IsInternalPath(msg.PkgPath); isInt { + return ErrInvalidPkgPath("pkgpath must not be of an internal package") + } if msg.Func == "" { // XXX return ErrInvalidExpr("missing function to call") } diff --git a/gno.land/pkg/sdk/vm/msg_test.go b/gno.land/pkg/sdk/vm/msgs_test.go similarity index 95% rename from gno.land/pkg/sdk/vm/msg_test.go rename to gno.land/pkg/sdk/vm/msgs_test.go index 684dc21e9f2..56b3931ef8d 100644 --- a/gno.land/pkg/sdk/vm/msg_test.go +++ b/gno.land/pkg/sdk/vm/msgs_test.go @@ -164,6 +164,20 @@ func TestMsgCall_ValidateBasic(t *testing.T) { }, expectErr: InvalidPkgPathError{}, }, + { + name: "pkgPath should not be an internal path", + msg: MsgCall{ + Caller: caller, + PkgPath: "gno.land/r/demo/avl/internal/sort", + Func: funcName, + Args: args, + Send: std.Coins{std.Coin{ + Denom: "ugnot", + Amount: 1000, + }}, + }, + expectErr: InvalidPkgPathError{}, + }, { name: "missing function name to call", msg: MsgCall{ diff --git a/gnovm/pkg/gnolang/helpers.go b/gnovm/pkg/gnolang/helpers.go index ddc1fd2fa55..5f0064cc4d9 100644 --- a/gnovm/pkg/gnolang/helpers.go +++ b/gnovm/pkg/gnolang/helpers.go @@ -28,6 +28,22 @@ func IsRealmPath(pkgPath string) bool { !ReGnoRunPath.MatchString(pkgPath) } +// IsInternalPath determines whether the given pkgPath refers to an internal +// package, that may not be called directly or imported by packages that don't +// share the same root. +// +// If isInternal is true, base will be set to the root of the internal package, +// which must also be an ancestor or the same path that imports the given +// internal package. +func IsInternalPath(pkgPath string) (base string, isInternal bool) { + // Restrict imports to /internal packages to a package rooted at base. + var suff string + base, suff, isInternal = strings.Cut(pkgPath, "/internal") + // /internal should be either at the end, or be a part: /internal/ + isInternal = isInternal && (suff == "" || suff[0] == '/') + return +} + // IsPurePackagePath determines whether the given pkgpath is for a published Gno package. // It only considers "pure" those starting with gno.land/p/, so it returns false for // stdlib packages and MsgRun paths. diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 14beec0d6e7..100f555b145 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4822,10 +4822,7 @@ func tryPredefine(store Store, pkg *PackageNode, last BlockNode, d Decl) (un Nam panic("cannot import stdlib internal/ package outside of standard library") } - // Restrict imports to /internal packages to a package rooted at base. - base, suff, isInternal := strings.Cut(d.PkgPath, "/internal") - // /internal should be either at the end, or be a part: /internal/ - isInternal = isInternal && (suff == "" || suff[0] == '/') + base, isInternal := IsInternalPath(d.PkgPath) if isInternal && pkg.PkgPath != base && !strings.HasPrefix(pkg.PkgPath, base+"/") { From 41a43baa6456fe38840eea4cc6c0896ffcc49f8d Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:07:46 +0100 Subject: [PATCH 4/8] feat(examples): add p/moul/fifo (#3736) Signed-off-by: moul <94029+moul@users.noreply.github.com> --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/fifo/fifo.gno | 246 ++++++++++++++++ examples/gno.land/p/moul/fifo/fifo_test.gno | 294 ++++++++++++++++++++ examples/gno.land/p/moul/fifo/gno.mod | 1 + 3 files changed, 541 insertions(+) create mode 100644 examples/gno.land/p/moul/fifo/fifo.gno create mode 100644 examples/gno.land/p/moul/fifo/fifo_test.gno create mode 100644 examples/gno.land/p/moul/fifo/gno.mod diff --git a/examples/gno.land/p/moul/fifo/fifo.gno b/examples/gno.land/p/moul/fifo/fifo.gno new file mode 100644 index 00000000000..d0291a941c0 --- /dev/null +++ b/examples/gno.land/p/moul/fifo/fifo.gno @@ -0,0 +1,246 @@ +// Package fifo implements a fixed-size FIFO (First-In-First-Out) list data structure +// using a singly-linked list. The implementation prioritizes storage efficiency by minimizing +// storage operations - each add/remove operation only updates 1-2 pointers, regardless of +// list size. +// +// Key features: +// - Fixed-size with automatic removal of oldest entries when full +// - Support for both prepend (add at start) and append (add at end) operations +// - Constant storage usage through automatic pruning +// - O(1) append operations and latest element access +// - Iterator support for sequential access +// - Dynamic size adjustment via SetMaxSize +// +// This implementation is optimized for frequent updates, as insertions and deletions only +// require updating 1-2 pointers. However, random access operations are O(n) as they require +// traversing the list. For use cases where writes are rare, a slice-based +// implementation might be more suitable. +// +// The linked list structure is equally efficient for storing both small values (like pointers) +// and larger data structures, as each node maintains a single next-pointer regardless of the +// stored value's size. +// +// Example usage: +// +// list := fifo.New(3) // Create a new list with max size 3 +// list.Append("a") // List: [a] +// list.Append("b") // List: [a b] +// list.Append("c") // List: [a b c] +// list.Append("d") // List: [b c d] (oldest element "a" was removed) +// latest := list.Latest() // Returns "d" +// all := list.Entries() // Returns ["b", "c", "d"] +package fifo + +// node represents a single element in the linked list +type node struct { + value interface{} + next *node +} + +// List represents a fixed-size FIFO list +type List struct { + head *node + tail *node + size int + maxSize int +} + +// New creates a new FIFO list with the specified maximum size +func New(maxSize int) *List { + return &List{ + maxSize: maxSize, + } +} + +// Prepend adds a new entry at the start of the list. If the list exceeds maxSize, +// the last entry is automatically removed. +func (l *List) Prepend(entry interface{}) { + if l.maxSize == 0 { + return + } + + newNode := &node{value: entry} + + if l.head == nil { + l.head = newNode + l.tail = newNode + l.size = 1 + return + } + + newNode.next = l.head + l.head = newNode + + if l.size < l.maxSize { + l.size++ + } else { + // Remove last element by traversing to second-to-last + if l.size == 1 { + // Special case: if size is 1, just update both pointers + l.head = newNode + l.tail = newNode + newNode.next = nil + } else { + // Find second-to-last node + current := l.head + for current.next != l.tail { + current = current.next + } + current.next = nil + l.tail = current + } + } +} + +// Append adds a new entry at the end of the list. If the list exceeds maxSize, +// the first entry is automatically removed. +func (l *List) Append(entry interface{}) { + if l.maxSize == 0 { + return + } + + newNode := &node{value: entry} + + if l.head == nil { + l.head = newNode + l.tail = newNode + l.size = 1 + return + } + + l.tail.next = newNode + l.tail = newNode + + if l.size < l.maxSize { + l.size++ + } else { + l.head = l.head.next + } +} + +// Get returns the entry at the specified index. +// Index 0 is the oldest entry, Size()-1 is the newest. +func (l *List) Get(index int) interface{} { + if index < 0 || index >= l.size { + return nil + } + + current := l.head + for i := 0; i < index; i++ { + current = current.next + } + return current.value +} + +// Size returns the current number of entries in the list +func (l *List) Size() int { + return l.size +} + +// MaxSize returns the maximum size configured for this list +func (l *List) MaxSize() int { + return l.maxSize +} + +// Entries returns all current entries as a slice +func (l *List) Entries() []interface{} { + entries := make([]interface{}, l.size) + current := l.head + for i := 0; i < l.size; i++ { + entries[i] = current.value + current = current.next + } + return entries +} + +// Iterator returns a function that can be used to iterate over the entries +// from oldest to newest. Returns nil when there are no more entries. +func (l *List) Iterator() func() interface{} { + current := l.head + return func() interface{} { + if current == nil { + return nil + } + value := current.value + current = current.next + return value + } +} + +// Latest returns the most recent entry. +// Returns nil if the list is empty. +func (l *List) Latest() interface{} { + if l.tail == nil { + return nil + } + return l.tail.value +} + +// SetMaxSize updates the maximum size of the list. +// If the new maxSize is smaller than the current size, +// the oldest entries are removed to fit the new size. +func (l *List) SetMaxSize(maxSize int) { + if maxSize < 0 { + maxSize = 0 + } + + // If new maxSize is smaller than current size, + // remove oldest entries until we fit + if maxSize < l.size { + // Special case: if new maxSize is 0, clear the list + if maxSize == 0 { + l.head = nil + l.tail = nil + l.size = 0 + } else { + // Keep the newest entries by moving head forward + diff := l.size - maxSize + for i := 0; i < diff; i++ { + l.head = l.head.next + } + l.size = maxSize + } + } + + l.maxSize = maxSize +} + +// Delete removes the element at the specified index. +// Returns true if an element was removed, false if the index was invalid. +func (l *List) Delete(index int) bool { + if index < 0 || index >= l.size { + return false + } + + // Special case: deleting the only element + if l.size == 1 { + l.head = nil + l.tail = nil + l.size = 0 + return true + } + + // Special case: deleting first element + if index == 0 { + l.head = l.head.next + l.size-- + return true + } + + // Find the node before the one to delete + current := l.head + for i := 0; i < index-1; i++ { + current = current.next + } + + // Special case: deleting last element + if index == l.size-1 { + l.tail = current + current.next = nil + } else { + current.next = current.next.next + } + + l.size-- + return true +} diff --git a/examples/gno.land/p/moul/fifo/fifo_test.gno b/examples/gno.land/p/moul/fifo/fifo_test.gno new file mode 100644 index 00000000000..1e3d27509c1 --- /dev/null +++ b/examples/gno.land/p/moul/fifo/fifo_test.gno @@ -0,0 +1,294 @@ +package fifo + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNew(t *testing.T) { + l := New(5) + uassert.Equal(t, 5, l.MaxSize()) + uassert.Equal(t, 0, l.Size()) +} + +func TestAppend(t *testing.T) { + l := New(3) + + // Test adding within capacity + l.Append(1) + l.Append(2) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, 2, l.Get(1)) + + // Test overflow behavior + l.Append(3) + l.Append(4) + uassert.Equal(t, 3, l.Size()) + uassert.Equal(t, 2, l.Get(0)) + uassert.Equal(t, 3, l.Get(1)) + uassert.Equal(t, 4, l.Get(2)) +} + +func TestPrepend(t *testing.T) { + l := New(3) + + // Test adding within capacity + l.Prepend(1) + l.Prepend(2) + uassert.Equal(t, 2, l.Size()) + uassert.Equal(t, 2, l.Get(0)) + uassert.Equal(t, 1, l.Get(1)) + + // Test overflow behavior + l.Prepend(3) + l.Prepend(4) + uassert.Equal(t, 3, l.Size()) + uassert.Equal(t, 4, l.Get(0)) + uassert.Equal(t, 3, l.Get(1)) + uassert.Equal(t, 2, l.Get(2)) +} + +func TestGet(t *testing.T) { + l := New(3) + l.Append(1) + l.Append(2) + l.Append(3) + + // Test valid indices + uassert.Equal(t, 1, l.Get(0)) + uassert.Equal(t, 2, l.Get(1)) + uassert.Equal(t, 3, l.Get(2)) + + // Test invalid indices + uassert.True(t, l.Get(-1) == nil) + uassert.True(t, l.Get(3) == nil) +} + +func TestEntries(t *testing.T) { + l := New(3) + l.Append(1) + l.Append(2) + l.Append(3) + + entries := l.Entries() + uassert.Equal(t, 3, len(entries)) + uassert.Equal(t, 1, entries[0]) + uassert.Equal(t, 2, entries[1]) + uassert.Equal(t, 3, entries[2]) +} + +func TestLatest(t *testing.T) { + l := New(5) + + // Test empty list + uassert.True(t, l.Latest() == nil) + + // Test single entry + l.Append(1) + uassert.Equal(t, 1, l.Latest()) + + // Test multiple entries + l.Append(2) + l.Append(3) + uassert.Equal(t, 3, l.Latest()) + + // Test after overflow + l.Append(4) + l.Append(5) + l.Append(6) + uassert.Equal(t, 6, l.Latest()) +} + +func TestIterator(t *testing.T) { + l := New(3) + l.Append(1) + l.Append(2) + l.Append(3) + + iter := l.Iterator() + uassert.Equal(t, 1, iter()) + uassert.Equal(t, 2, iter()) + uassert.Equal(t, 3, iter()) + uassert.True(t, iter() == nil) +} + +func TestMixedOperations(t *testing.T) { + l := New(3) + + // Mix of append and prepend operations + l.Append(1) // [1] + l.Prepend(2) // [2,1] + l.Append(3) // [2,1,3] + l.Prepend(4) // [4,2,1] + + entries := l.Entries() + uassert.Equal(t, 3, len(entries)) + uassert.Equal(t, 4, entries[0]) + uassert.Equal(t, 2, entries[1]) + uassert.Equal(t, 1, entries[2]) +} + +func TestEmptyList(t *testing.T) { + l := New(3) + + // Test operations on empty list + uassert.Equal(t, 0, l.Size()) + uassert.True(t, l.Get(0) == nil) + uassert.Equal(t, 0, len(l.Entries())) + uassert.True(t, l.Latest() == nil) + + iter := l.Iterator() + uassert.True(t, iter() == nil) +} + +func TestEdgeCases(t *testing.T) { + // Test zero-size list + l := New(0) + uassert.Equal(t, 0, l.MaxSize()) + l.Append(1) // Should be no-op + uassert.Equal(t, 0, l.Size()) + + // Test single-element list + l = New(1) + l.Append(1) + l.Append(2) // Should replace 1 + uassert.Equal(t, 1, l.Size()) + uassert.Equal(t, 2, l.Latest()) + + // Test rapid append/prepend alternation + l = New(3) + l.Append(1) // [1] + l.Prepend(2) // [2,1] + l.Append(3) // [2,1,3] + l.Prepend(4) // [4,2,1] + l.Append(5) // [2,1,5] + uassert.Equal(t, 3, l.Size()) + entries := l.Entries() + uassert.Equal(t, 2, entries[0]) + uassert.Equal(t, 1, entries[1]) + uassert.Equal(t, 5, entries[2]) + + // Test nil values + l = New(2) + l.Append(nil) + l.Prepend(nil) + uassert.Equal(t, 2, l.Size()) + uassert.True(t, l.Get(0) == nil) + uassert.True(t, l.Get(1) == nil) + + // Test index bounds + l = New(3) + l.Append(1) + uassert.True(t, l.Get(-1) == nil) + uassert.True(t, l.Get(1) == nil) + + // Test iterator exhaustion + l = New(2) + l.Append(1) + l.Append(2) + iter := l.Iterator() + uassert.Equal(t, 1, iter()) + uassert.Equal(t, 2, iter()) + uassert.True(t, iter() == nil) + uassert.True(t, iter() == nil) + + // Test prepend on full list + l = New(2) + l.Append(1) + l.Append(2) // [1,2] + l.Prepend(3) // [3,1] + uassert.Equal(t, 2, l.Size()) + entries = l.Entries() + uassert.Equal(t, 3, entries[0]) + uassert.Equal(t, 1, entries[1]) +} + +func TestSetMaxSize(t *testing.T) { + l := New(5) + + // Fill the list + l.Append(1) + l.Append(2) + l.Append(3) + l.Append(4) + l.Append(5) + + // Test increasing maxSize + l.SetMaxSize(7) + uassert.Equal(t, 7, l.MaxSize()) + uassert.Equal(t, 5, l.Size()) + + // Test reducing maxSize + l.SetMaxSize(3) + uassert.Equal(t, 3, l.Size()) + entries := l.Entries() + uassert.Equal(t, 3, entries[0]) + uassert.Equal(t, 4, entries[1]) + uassert.Equal(t, 5, entries[2]) + + // Test setting to zero + l.SetMaxSize(0) + uassert.Equal(t, 0, l.Size()) + uassert.True(t, l.head == nil) + uassert.True(t, l.tail == nil) + + // Test negative maxSize + l.SetMaxSize(-1) + uassert.Equal(t, 0, l.MaxSize()) + + // Test setting back to positive + l.SetMaxSize(2) + l.Append(1) + l.Append(2) + l.Append(3) + uassert.Equal(t, 2, l.Size()) + entries = l.Entries() + uassert.Equal(t, 2, entries[0]) + uassert.Equal(t, 3, entries[1]) +} + +func TestDelete(t *testing.T) { + l := New(5) + + // Test delete on empty list + uassert.False(t, l.Delete(0)) + uassert.False(t, l.Delete(-1)) + + // Fill list + l.Append(1) + l.Append(2) + l.Append(3) + l.Append(4) + + // Test invalid indices + uassert.False(t, l.Delete(-1)) + uassert.False(t, l.Delete(4)) + + // Test deleting from middle + uassert.True(t, l.Delete(1)) + uassert.Equal(t, 3, l.Size()) + entries := l.Entries() + uassert.Equal(t, 1, entries[0]) + uassert.Equal(t, 3, entries[1]) + uassert.Equal(t, 4, entries[2]) + + // Test deleting from head + uassert.True(t, l.Delete(0)) + uassert.Equal(t, 2, l.Size()) + entries = l.Entries() + uassert.Equal(t, 3, entries[0]) + uassert.Equal(t, 4, entries[1]) + + // Test deleting from tail + uassert.True(t, l.Delete(1)) + uassert.Equal(t, 1, l.Size()) + uassert.Equal(t, 3, l.Latest()) + + // Test deleting last element + uassert.True(t, l.Delete(0)) + uassert.Equal(t, 0, l.Size()) + uassert.True(t, l.head == nil) + uassert.True(t, l.tail == nil) +} diff --git a/examples/gno.land/p/moul/fifo/gno.mod b/examples/gno.land/p/moul/fifo/gno.mod new file mode 100644 index 00000000000..dccbc39453b --- /dev/null +++ b/examples/gno.land/p/moul/fifo/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/fifo From c3c6e66b2557c0ebc66c4505bba33e24a1ac9a90 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Mon, 24 Feb 2025 18:38:23 +0200 Subject: [PATCH 5/8] fix(gnolang): prescriptively reject *ast.(IndexListExpr, GoStmt) as unrecognized for Gno (#3752) The *ast.IndexListExpr is used for generics but in assignment operations it is illegal to use. This change returns a proper error and matches Go's output. Also *ast.GoStmt is for spawning Go routines but those are forbidden in Gno, hence reject them prescriptively instead of just spewing out the raw ast type. Fixes #3731 Updates #3751 --------- Co-authored-by: Morgan --- gnovm/pkg/gnolang/go2gno.go | 7 +++++++ gnovm/tests/files/parse_err1.gno | 16 ++++++++++++++++ gnovm/tests/files/parse_err2.gno | 16 ++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 gnovm/tests/files/parse_err1.gno create mode 100644 gnovm/tests/files/parse_err2.gno diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index b98136b13b5..8379bfef425 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -478,6 +478,13 @@ func Go2Gno(fs *token.FileSet, gon ast.Node) (n Node) { } case *ast.EmptyStmt: return &EmptyStmt{} + case *ast.IndexListExpr: + if len(gon.Indices) > 1 { + panicWithPos("invalid operation: more than one index") + } + panicWithPos("invalid operation: indexList is not permitted in Gno") + case *ast.GoStmt: + panicWithPos("goroutines are not permitted") default: panicWithPos("unknown Go type %v: %s\n", reflect.TypeOf(gon), diff --git a/gnovm/tests/files/parse_err1.gno b/gnovm/tests/files/parse_err1.gno new file mode 100644 index 00000000000..efa21e883b0 --- /dev/null +++ b/gnovm/tests/files/parse_err1.gno @@ -0,0 +1,16 @@ +// https://github.com/gnolang/gno/issues/3731 + +package main + +type node struct { + r []int +} + +func (n *node) foo(targ, wndex int) { + _ = n.r[targ, wndex] +} + +func main() {} + +// Error: +// files/parse_err1.gno:10:6: invalid operation: more than one index diff --git a/gnovm/tests/files/parse_err2.gno b/gnovm/tests/files/parse_err2.gno new file mode 100644 index 00000000000..3e0f79d6935 --- /dev/null +++ b/gnovm/tests/files/parse_err2.gno @@ -0,0 +1,16 @@ +// https://github.com/gnolang/gno/issues/3751 + +package math + +import "testing" + +func Add(a, b int) int { + return a + b +} + +func TestAdd(t *testing.T) { + go Add(1, 1) +} + +// Error: +// files/parse_err2.gno:12:2: goroutines are not permitted From 6803b7741ce6862d2704ffc1d4de280d518b16e7 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 24 Feb 2025 17:43:37 +0100 Subject: [PATCH 6/8] fix(gnovm): remove `preprocessing` variable (#3728) This PR removes the `preprocessing` global variable. This was only used in one function; now that we have `PreprocessingMode` in the machine, we can move this check out of the specific function and into the places where we need to check it --- gnovm/pkg/gnolang/op_assign.go | 6 ++++ gnovm/pkg/gnolang/op_decl.go | 44 +++++++++++-------------- gnovm/pkg/gnolang/uverse.go | 3 ++ gnovm/pkg/gnolang/values_conversions.go | 14 -------- 4 files changed, 29 insertions(+), 38 deletions(-) diff --git a/gnovm/pkg/gnolang/op_assign.go b/gnovm/pkg/gnolang/op_assign.go index 5e841fb18fd..b42bb4744c0 100644 --- a/gnovm/pkg/gnolang/op_assign.go +++ b/gnovm/pkg/gnolang/op_assign.go @@ -20,6 +20,9 @@ func (m *Machine) doOpDefine() { } } } + if !m.PreprocessorMode && isUntyped(rvs[i].T) && rvs[i].T.Kind() != BoolKind { + panic("untyped conversion should not happen at runtime") + } ptr.Assign2(m.Alloc, m.Store, m.Realm, rvs[i], true) } } @@ -41,6 +44,9 @@ func (m *Machine) doOpAssign() { } } } + if !m.PreprocessorMode && isUntyped(rvs[i].T) && rvs[i].T.Kind() != BoolKind { + panic("untyped conversion should not happen at runtime") + } lv.Assign2(m.Alloc, m.Store, m.Realm, rvs[i], true) } } diff --git a/gnovm/pkg/gnolang/op_decl.go b/gnovm/pkg/gnolang/op_decl.go index c9c04ccf76d..6dbae2d3edf 100644 --- a/gnovm/pkg/gnolang/op_decl.go +++ b/gnovm/pkg/gnolang/op_decl.go @@ -29,35 +29,31 @@ func (m *Machine) doOpValueDecl() { } else { tv = rvs[i] } - if nt != nil { - if nt.Kind() == InterfaceKind { - if isUntyped(tv.T) { - ConvertUntypedTo(&tv, nil) - } else { - // keep type as is. + + if isUntyped(tv.T) { + if !s.Const { + if !m.PreprocessorMode && rvs[i].T.Kind() != BoolKind { + panic("untyped conversion should not happen at runtime") } - } else { - if isUntyped(tv.T) { - ConvertUntypedTo(&tv, nt) - } else { - if debug { - if nt.TypeID() != tv.T.TypeID() && - baseOf(nt).TypeID() != tv.T.TypeID() { - panic(fmt.Sprintf( - "type mismatch: %s vs %s", - nt.TypeID(), - tv.T.TypeID(), - )) - } + ConvertUntypedTo(&tv, nil) + } + } else if nt != nil { + // if nt.T is an interface, maintain tv.T as-is. + if nt.Kind() != InterfaceKind { + if debug { + if nt.TypeID() != tv.T.TypeID() && + baseOf(nt).TypeID() != tv.T.TypeID() { + panic(fmt.Sprintf( + "type mismatch: %s vs %s", + nt.TypeID(), + tv.T.TypeID(), + )) } - tv.T = nt } + tv.T = nt } - } else if s.Const { - // leave untyped as is. - } else if isUntyped(tv.T) { - ConvertUntypedTo(&tv, nil) } + nx := &s.NameExprs[i] ptr := lb.GetPointerToMaybeHeapDefine(m.Store, nx) ptr.Assign2(m.Alloc, m.Store, m.Realm, tv, false) diff --git a/gnovm/pkg/gnolang/uverse.go b/gnovm/pkg/gnolang/uverse.go index 55791ba4900..074f7a51b66 100644 --- a/gnovm/pkg/gnolang/uverse.go +++ b/gnovm/pkg/gnolang/uverse.go @@ -1050,6 +1050,9 @@ func makeUverseNode() { } } + if isUntyped(exception.Value.T) { + ConvertUntypedTo(&exception.Value, nil) + } m.PushValue(exception.Value) // Recover complete; remove exceptions. m.Exceptions = nil diff --git a/gnovm/pkg/gnolang/values_conversions.go b/gnovm/pkg/gnolang/values_conversions.go index 920ed655ec9..ce256680de6 100644 --- a/gnovm/pkg/gnolang/values_conversions.go +++ b/gnovm/pkg/gnolang/values_conversions.go @@ -1316,28 +1316,14 @@ func ConvertUntypedTo(tv *TypedValue, t Type) { } switch tv.T { case UntypedBoolType: - if debug { - if t.Kind() != BoolKind { - panic("untyped bool can only be converted to bool kind") - } - } tv.T = t case UntypedRuneType: ConvertUntypedRuneTo(tv, t) case UntypedBigintType: - if preprocessing.Load() == 0 { - panic("untyped Bigint conversion should not happen during interpretation") - } ConvertUntypedBigintTo(tv, tv.V.(BigintValue), t) case UntypedBigdecType: - if preprocessing.Load() == 0 { - panic("untyped Bigdec conversion should not happen during interpretation") - } ConvertUntypedBigdecTo(tv, tv.V.(BigdecValue), t) case UntypedStringType: - if preprocessing.Load() == 0 { - panic("untyped String conversion should not happen during interpretation") - } if t.Kind() == StringKind { tv.T = t return From 83c83f9e7b5fdf04b3ac5f3fd3d0fba47563f13e Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:05:10 +0100 Subject: [PATCH 7/8] feat(examples): add p/moul/authz (#3700) We should probably replace most of the current Ownable-based contracts with this option. This way, we can start with a central admin and later transition to a DAO. --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/authz/authz.gno | 261 ++++++++++++ examples/gno.land/p/moul/authz/authz_test.gno | 384 ++++++++++++++++++ .../gno.land/p/moul/authz/example_test.gno | 88 ++++ examples/gno.land/p/moul/authz/gno.mod | 1 + examples/gno.land/p/moul/once/gno.mod | 1 + examples/gno.land/p/moul/once/once.gno | 100 +++++ examples/gno.land/p/moul/once/once_test.gno | 231 +++++++++++ .../pkg/integration/testdata/moul_authz.txtar | 82 ++++ 8 files changed, 1148 insertions(+) create mode 100644 examples/gno.land/p/moul/authz/authz.gno create mode 100644 examples/gno.land/p/moul/authz/authz_test.gno create mode 100644 examples/gno.land/p/moul/authz/example_test.gno create mode 100644 examples/gno.land/p/moul/authz/gno.mod create mode 100644 examples/gno.land/p/moul/once/gno.mod create mode 100644 examples/gno.land/p/moul/once/once.gno create mode 100644 examples/gno.land/p/moul/once/once_test.gno create mode 100644 gno.land/pkg/integration/testdata/moul_authz.txtar diff --git a/examples/gno.land/p/moul/authz/authz.gno b/examples/gno.land/p/moul/authz/authz.gno new file mode 100644 index 00000000000..059d1234b10 --- /dev/null +++ b/examples/gno.land/p/moul/authz/authz.gno @@ -0,0 +1,261 @@ +// Package authz provides flexible authorization control for privileged actions. +// +// # Authorization Strategies +// +// The package supports multiple authorization strategies: +// - Member-based: Single user or team of users +// - Contract-based: Async authorization (e.g., via DAO) +// - Auto-accept: Allow all actions +// - Drop: Deny all actions +// +// Core Components +// +// - Authority interface: Base interface implemented by all authorities +// - Authorizer: Main wrapper object for authority management +// - MemberAuthority: Manages authorized addresses +// - ContractAuthority: Delegates to another contract +// - AutoAcceptAuthority: Accepts all actions +// - DroppedAuthority: Denies all actions +// +// Quick Start +// +// // Initialize with contract deployer as authority +// var auth = authz.New() +// +// // Create functions that require authorization +// func UpdateConfig(newValue string) error { +// return auth.Do("update_config", func() error { +// config = newValue +// return nil +// }) +// } +// +// See example_test.gno for more usage examples. +package authz + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/avl/rotree" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/addrset" + "gno.land/p/moul/once" +) + +// Authorizer is the main wrapper object that handles authority management +type Authorizer struct { + current Authority +} + +// Authority represents an entity that can authorize privileged actions +type Authority interface { + // Authorize executes a privileged action if the caller is authorized + // Additional args can be provided for context (e.g., for proposal creation) + Authorize(title string, action PrivilegedAction, args ...interface{}) error + + // String returns a human-readable description of the authority + String() string +} + +// PrivilegedAction defines a function that performs a privileged action. +type PrivilegedAction func() error + +// PrivilegedActionHandler is called by contract-based authorities to handle +// privileged actions. +type PrivilegedActionHandler func(title string, action PrivilegedAction) error + +// New creates a new Authorizer with the current realm's address as authority +func New() *Authorizer { + return &Authorizer{ + current: NewMemberAuthority(std.PreviousRealm().Address()), + } +} + +// NewWithAuthority creates a new Authorizer with a specific authority +func NewWithAuthority(authority Authority) *Authorizer { + return &Authorizer{ + current: authority, + } +} + +// Current returns the current authority implementation +func (a *Authorizer) Current() Authority { + return a.current +} + +// Transfer changes the current authority after validation +func (a *Authorizer) Transfer(newAuthority Authority) error { + // Ask current authority to validate the transfer + return a.current.Authorize("transfer_authority", func() error { + a.current = newAuthority + return nil + }) +} + +// Do executes a privileged action through the current authority +func (a *Authorizer) Do(title string, action PrivilegedAction, args ...interface{}) error { + return a.current.Authorize(title, action, args...) +} + +// String returns a string representation of the current authority +func (a *Authorizer) String() string { + return a.current.String() +} + +// MemberAuthority is the default implementation using addrset for member management +type MemberAuthority struct { + members addrset.Set +} + +func NewMemberAuthority(members ...std.Address) *MemberAuthority { + auth := &MemberAuthority{} + for _, addr := range members { + auth.members.Add(addr) + } + return auth +} + +func (a *MemberAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + caller := std.PreviousRealm().Address() + if !a.members.Has(caller) { + return errors.New("unauthorized") + } + + if err := action(); err != nil { + return err + } + return nil +} + +func (a *MemberAuthority) String() string { + return ufmt.Sprintf("member_authority[size=%d]", a.members.Size()) +} + +// AddMember adds a new member to the authority +func (a *MemberAuthority) AddMember(addr std.Address) error { + return a.Authorize("add_member", func() error { + a.members.Add(addr) + return nil + }) +} + +// RemoveMember removes a member from the authority +func (a *MemberAuthority) RemoveMember(addr std.Address) error { + return a.Authorize("remove_member", func() error { + a.members.Remove(addr) + return nil + }) +} + +// Tree returns a read-only view of the members tree +func (a *MemberAuthority) Tree() *rotree.ReadOnlyTree { + tree := a.members.Tree().(*avl.Tree) + return rotree.Wrap(tree, nil) +} + +// Has checks if the given address is a member of the authority +func (a *MemberAuthority) Has(addr std.Address) bool { + return a.members.Has(addr) +} + +// ContractAuthority implements async contract-based authority +type ContractAuthority struct { + contractPath string + contractAddr std.Address + contractHandler PrivilegedActionHandler + proposer Authority // controls who can create proposals + executionOnce once.Once +} + +func NewContractAuthority(path string, handler PrivilegedActionHandler) *ContractAuthority { + return &ContractAuthority{ + contractPath: path, + contractAddr: std.DerivePkgAddr(path), + contractHandler: handler, + proposer: NewAutoAcceptAuthority(), // default: anyone can propose + executionOnce: once.Once{}, // initialize execution once + } +} + +// NewRestrictedContractAuthority creates a new contract authority with a proposer restriction +func NewRestrictedContractAuthority(path string, handler PrivilegedActionHandler, proposer Authority) Authority { + if path == "" { + panic("contract path cannot be empty") + } + if handler == nil { + panic("contract handler cannot be nil") + } + if proposer == nil { + panic("proposer cannot be nil") + } + return &ContractAuthority{ + contractPath: path, + contractAddr: std.DerivePkgAddr(path), + contractHandler: handler, + proposer: proposer, + executionOnce: once.Once{}, + } +} + +func (a *ContractAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + if a.contractHandler == nil { + return errors.New("contract handler is not set") + } + + // Wrap the action to ensure it can only be executed by the contract + wrappedAction := func() error { + caller := std.PreviousRealm().Address() + if caller != a.contractAddr { + return errors.New("action can only be executed by the contract") + } + return a.executionOnce.DoErr(func() error { + return action() + }) + } + + // Use the proposer authority to control who can create proposals + return a.proposer.Authorize(title+"_proposal", func() error { + if err := a.contractHandler(title, wrappedAction); err != nil { + return err + } + return nil + }, args...) +} + +func (a *ContractAuthority) String() string { + return ufmt.Sprintf("contract_authority[contract=%s]", a.contractPath) +} + +// AutoAcceptAuthority implements an authority that accepts all actions +// AutoAcceptAuthority is a simple authority that automatically accepts all actions. +// It can be used as a proposer authority to allow anyone to create proposals. +type AutoAcceptAuthority struct{} + +func NewAutoAcceptAuthority() *AutoAcceptAuthority { + return &AutoAcceptAuthority{} +} + +func (a *AutoAcceptAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + return action() +} + +func (a *AutoAcceptAuthority) String() string { + return "auto_accept_authority" +} + +// droppedAuthority implements an authority that denies all actions +type droppedAuthority struct{} + +func NewDroppedAuthority() Authority { + return &droppedAuthority{} +} + +func (a *droppedAuthority) Authorize(title string, action PrivilegedAction, args ...interface{}) error { + return errors.New("dropped authority: all actions are denied") +} + +func (a *droppedAuthority) String() string { + return "dropped_authority" +} diff --git a/examples/gno.land/p/moul/authz/authz_test.gno b/examples/gno.land/p/moul/authz/authz_test.gno new file mode 100644 index 00000000000..f8915cc2448 --- /dev/null +++ b/examples/gno.land/p/moul/authz/authz_test.gno @@ -0,0 +1,384 @@ +package authz + +import ( + "errors" + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" +) + +func TestNew(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Check that the current authority is a MemberAuthority + memberAuth, ok := auth.Current().(*MemberAuthority) + uassert.True(t, ok, "expected MemberAuthority") + + // Check that the caller is a member + uassert.True(t, memberAuth.Has(testAddr), "caller should be a member") + + // Check string representation + expected := ufmt.Sprintf("member_authority[size=%d]", 1) + uassert.Equal(t, expected, auth.String()) +} + +func TestNewWithAuthority(t *testing.T) { + testAddr := testutils.TestAddress("alice") + memberAuth := NewMemberAuthority(testAddr) + + auth := NewWithAuthority(memberAuth) + + // Check that the current authority is the one we provided + uassert.True(t, auth.Current() == memberAuth, "expected provided authority") +} + +func TestAuthorizerAuthorize(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Test successful action with args + executed := false + args := []interface{}{"test_arg", 123} + err := auth.Do("test_action", func() error { + executed = true + return nil + }, args...) + + uassert.True(t, err == nil, "expected no error") + uassert.True(t, executed, "action should have been executed") + + // Test unauthorized action with args + std.TestSetOriginCaller(testutils.TestAddress("bob")) + + executed = false + err = auth.Do("test_action", func() error { + executed = true + return nil + }, "unauthorized_arg") + + uassert.True(t, err != nil, "expected error") + uassert.False(t, executed, "action should not have been executed") + + // Test action returning error + std.TestSetOriginCaller(testAddr) + expectedErr := errors.New("test error") + + err = auth.Do("test_action", func() error { + return expectedErr + }) + + uassert.True(t, err == expectedErr, "expected specific error") +} + +func TestAuthorizerTransfer(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Test transfer to new member authority + newAddr := testutils.TestAddress("bob") + newAuth := NewMemberAuthority(newAddr) + + err := auth.Transfer(newAuth) + uassert.True(t, err == nil, "expected no error") + uassert.True(t, auth.Current() == newAuth, "expected new authority") + + // Test unauthorized transfer + std.TestSetOriginCaller(testutils.TestAddress("carol")) + + err = auth.Transfer(NewMemberAuthority(testAddr)) + uassert.True(t, err != nil, "expected error") + + // Test transfer to contract authority + std.TestSetOriginCaller(newAddr) + + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + return action() + }) + + err = auth.Transfer(contractAuth) + uassert.True(t, err == nil, "expected no error") + uassert.True(t, auth.Current() == contractAuth, "expected contract authority") +} + +func TestAuthorizerTransferChain(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + // Create a chain of transfers + auth := New() + + // First transfer to a new member authority + newAddr := testutils.TestAddress("bob") + memberAuth := NewMemberAuthority(newAddr) + + err := auth.Transfer(memberAuth) + uassert.True(t, err == nil, "unexpected error in first transfer") + + // Then transfer to a contract authority + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + return action() + }) + + std.TestSetOriginCaller(newAddr) + err = auth.Transfer(contractAuth) + uassert.True(t, err == nil, "unexpected error in second transfer") + + // Finally transfer to an auto-accept authority + autoAuth := NewAutoAcceptAuthority() + + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = auth.Transfer(autoAuth) + uassert.True(t, err == nil, "unexpected error in final transfer") + uassert.True(t, auth.Current() == autoAuth, "expected auto-accept authority") +} + +func TestAuthorizerWithDroppedAuthority(t *testing.T) { + testAddr := testutils.TestAddress("alice") + std.TestSetOriginCaller(testAddr) + + auth := New() + + // Transfer to dropped authority + err := auth.Transfer(NewDroppedAuthority()) + uassert.True(t, err == nil, "expected no error") + + // Try to execute action + err = auth.Do("test_action", func() error { + return nil + }) + uassert.True(t, err != nil, "expected error from dropped authority") + + // Try to transfer again + err = auth.Transfer(NewMemberAuthority(testAddr)) + uassert.True(t, err != nil, "expected error when transferring from dropped authority") +} + +func TestContractAuthorityExecutionOnce(t *testing.T) { + attempts := 0 + executed := 0 + var capturedArgs []interface{} + + contractAuth := NewContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + // Try to execute the action twice in the same handler + if err := action(); err != nil { + return err + } + attempts++ + + // Second execution should fail + if err := action(); err != nil { + return err + } + attempts++ + return nil + }) + + // Set caller to contract address + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + + testArgs := []interface{}{"proposal_id", 42, "metadata", map[string]string{"key": "value"}} + err := contractAuth.Authorize("test_action", func() error { + executed++ + return nil + }, testArgs...) + + uassert.True(t, err == nil, "handler execution should succeed") + uassert.True(t, attempts == 2, "handler should have attempted execution twice") + uassert.True(t, executed == 1, "handler should have executed once") +} + +func TestContractAuthorityWithProposer(t *testing.T) { + testAddr := testutils.TestAddress("alice") + memberAuth := NewMemberAuthority(testAddr) + + handlerCalled := false + actionExecuted := false + + contractAuth := NewRestrictedContractAuthority("gno.land/r/test", func(title string, action PrivilegedAction) error { + handlerCalled = true + // Set caller to contract address before executing action + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + return action() + }, memberAuth) + + // Test authorized member + std.TestSetOriginCaller(testAddr) + testArgs := []interface{}{"proposal_metadata", "test value"} + err := contractAuth.Authorize("test_action", func() error { + actionExecuted = true + return nil + }, testArgs...) + + uassert.True(t, err == nil, "authorized member should be able to propose") + uassert.True(t, handlerCalled, "contract handler should be called") + uassert.True(t, actionExecuted, "action should be executed") + + // Reset flags for unauthorized test + handlerCalled = false + actionExecuted = false + + // Test unauthorized proposer + std.TestSetOriginCaller(testutils.TestAddress("bob")) + err = contractAuth.Authorize("test_action", func() error { + actionExecuted = true + return nil + }, testArgs...) + + uassert.True(t, err != nil, "unauthorized member should not be able to propose") + uassert.False(t, handlerCalled, "contract handler should not be called for unauthorized proposer") + uassert.False(t, actionExecuted, "action should not be executed for unauthorized proposer") +} + +func TestAutoAcceptAuthority(t *testing.T) { + auth := NewAutoAcceptAuthority() + + // Test that any action is authorized + executed := false + err := auth.Authorize("test_action", func() error { + executed = true + return nil + }) + + uassert.True(t, err == nil, "auto-accept should not return error") + uassert.True(t, executed, "action should have been executed") + + // Test with different caller + std.TestSetOriginCaller(testutils.TestAddress("random")) + executed = false + err = auth.Authorize("test_action", func() error { + executed = true + return nil + }) + + uassert.True(t, err == nil, "auto-accept should not care about caller") + uassert.True(t, executed, "action should have been executed") +} + +func TestAutoAcceptAuthorityWithArgs(t *testing.T) { + auth := NewAutoAcceptAuthority() + + // Test that any action is authorized with args + executed := false + testArgs := []interface{}{"arg1", 42, "arg3"} + err := auth.Authorize("test_action", func() error { + executed = true + return nil + }, testArgs...) + + uassert.True(t, err == nil, "auto-accept should not return error") + uassert.True(t, executed, "action should have been executed") +} + +func TestMemberAuthorityMultipleMembers(t *testing.T) { + alice := testutils.TestAddress("alice") + bob := testutils.TestAddress("bob") + carol := testutils.TestAddress("carol") + + // Create authority with multiple members + auth := NewMemberAuthority(alice, bob) + + // Test that both members can execute actions + for _, member := range []std.Address{alice, bob} { + std.TestSetOriginCaller(member) + err := auth.Authorize("test_action", func() error { + return nil + }) + uassert.True(t, err == nil, "member should be authorized") + } + + // Test that non-member cannot execute + std.TestSetOriginCaller(carol) + err := auth.Authorize("test_action", func() error { + return nil + }) + uassert.True(t, err != nil, "non-member should not be authorized") + + // Test Tree() functionality + tree := auth.Tree() + uassert.True(t, tree.Size() == 2, "tree should have 2 members") + + // Verify both members are in the tree + found := make(map[std.Address]bool) + tree.Iterate("", "", func(key string, _ interface{}) bool { + found[std.Address(key)] = true + return false + }) + uassert.True(t, found[alice], "alice should be in the tree") + uassert.True(t, found[bob], "bob should be in the tree") + uassert.False(t, found[carol], "carol should not be in the tree") + + // Test read-only nature of the tree + defer func() { + r := recover() + uassert.True(t, r != nil, "modifying read-only tree should panic") + }() + tree.Set(string(carol), nil) // This should panic +} + +func TestAuthorizerCurrentNeverNil(t *testing.T) { + auth := New() + + // Current should never be nil after initialization + uassert.True(t, auth.Current() != nil, "current authority should not be nil") + + // Current should not be nil after transfer + err := auth.Transfer(NewAutoAcceptAuthority()) + uassert.True(t, err == nil, "transfer should succeed") + uassert.True(t, auth.Current() != nil, "current authority should not be nil after transfer") +} + +func TestAuthorizerString(t *testing.T) { + auth := New() + + // Test initial string representation + str := auth.String() + uassert.True(t, str != "", "string representation should not be empty") + + // Test string after transfer + autoAuth := NewAutoAcceptAuthority() + err := auth.Transfer(autoAuth) + uassert.True(t, err == nil, "transfer should succeed") + + newStr := auth.String() + uassert.True(t, newStr != "", "string representation should not be empty") + uassert.True(t, newStr != str, "string should change after transfer") +} + +func TestContractAuthorityValidation(t *testing.T) { + // Test empty path + auth := NewContractAuthority("", nil) + std.TestSetOriginCaller(std.DerivePkgAddr("")) + err := auth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err != nil, "empty path authority should fail to authorize") + + // Test nil handler + auth = NewContractAuthority("gno.land/r/test", nil) + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = auth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err != nil, "nil handler authority should fail to authorize") + + // Test valid configuration + handler := func(title string, action PrivilegedAction) error { + return nil + } + contractAuth := NewContractAuthority("gno.land/r/test", handler) + std.TestSetOriginCaller(std.DerivePkgAddr("gno.land/r/test")) + err = contractAuth.Authorize("test", func() error { + return nil + }) + uassert.True(t, err == nil, "valid contract authority should authorize successfully") +} diff --git a/examples/gno.land/p/moul/authz/example_test.gno b/examples/gno.land/p/moul/authz/example_test.gno new file mode 100644 index 00000000000..a283204dfc2 --- /dev/null +++ b/examples/gno.land/p/moul/authz/example_test.gno @@ -0,0 +1,88 @@ +package authz + +import ( + "std" +) + +// Example_basic demonstrates initializing and using a basic member authority +func Example_basic() { + // Initialize with contract deployer as authority + auth := New() + + // Use the authority to perform a privileged action + auth.Do("update_config", func() error { + // config = newValue + return nil + }) +} + +// Example_addingMembers demonstrates how to add new members to a member authority +func Example_addingMembers() { + // Initialize with contract deployer as authority + auth := New() + + // Add a new member to the authority + memberAuth := auth.Current().(*MemberAuthority) + memberAuth.AddMember(std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")) +} + +// Example_contractAuthority demonstrates using a contract-based authority +func Example_contractAuthority() { + // Initialize with contract authority (e.g., DAO) + auth := NewWithAuthority( + NewContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, // defined elsewhere for example + ), + ) + + // Privileged actions will be handled by the contract + auth.Do("update_params", func() error { + // Executes after DAO approval + return nil + }) +} + +// Example_restrictedContractAuthority demonstrates a contract authority with member-only proposals +func Example_restrictedContractAuthority() { + // Initialize member authority for proposers + proposerAuth := NewMemberAuthority( + std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"), // admin1 + std.Address("g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"), // admin2 + ) + + // Create contract authority with restricted proposers + auth := NewWithAuthority( + NewRestrictedContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, + proposerAuth, + ), + ) + + // Only members can propose, and contract must approve + auth.Do("update_params", func() error { + // Executes after: + // 1. Proposer initiates + // 2. DAO approves + return nil + }) +} + +// Example_switchingAuthority demonstrates switching from member to contract authority +func Example_switchingAuthority() { + // Start with member authority (deployer) + auth := New() + + // Create and switch to contract authority + daoAuthority := NewContractAuthority( + "gno.land/r/demo/dao", + mockDAOHandler, + ) + auth.Transfer(daoAuthority) +} + +// Mock handler for examples +func mockDAOHandler(title string, action PrivilegedAction) error { + return action() +} diff --git a/examples/gno.land/p/moul/authz/gno.mod b/examples/gno.land/p/moul/authz/gno.mod new file mode 100644 index 00000000000..2ae057d875b --- /dev/null +++ b/examples/gno.land/p/moul/authz/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/authz diff --git a/examples/gno.land/p/moul/once/gno.mod b/examples/gno.land/p/moul/once/gno.mod new file mode 100644 index 00000000000..29afe6841b2 --- /dev/null +++ b/examples/gno.land/p/moul/once/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/once diff --git a/examples/gno.land/p/moul/once/once.gno b/examples/gno.land/p/moul/once/once.gno new file mode 100644 index 00000000000..b9b6054c037 --- /dev/null +++ b/examples/gno.land/p/moul/once/once.gno @@ -0,0 +1,100 @@ +// Package once provides utilities for one-time execution patterns. +// It extends the concept of sync.Once with error handling and panic options. +package once + +import ( + "errors" +) + +// Once represents a one-time execution guard +type Once struct { + done bool + err error + paniced bool + value interface{} // stores the result of the execution +} + +// New creates a new Once instance +func New() *Once { + return &Once{} +} + +// Do executes fn only once and returns nil on subsequent calls +func (o *Once) Do(fn func()) { + if o.done { + return + } + defer func() { o.done = true }() + fn() +} + +// DoErr executes fn only once and returns the same error on subsequent calls +func (o *Once) DoErr(fn func() error) error { + if o.done { + return o.err + } + defer func() { o.done = true }() + o.err = fn() + return o.err +} + +// DoOrPanic executes fn only once and panics on subsequent calls +func (o *Once) DoOrPanic(fn func()) { + if o.done { + panic("once: multiple execution attempted") + } + defer func() { o.done = true }() + fn() +} + +// DoValue executes fn only once and returns its value, subsequent calls return the cached value +func (o *Once) DoValue(fn func() interface{}) interface{} { + if o.done { + return o.value + } + defer func() { o.done = true }() + o.value = fn() + return o.value +} + +// DoValueErr executes fn only once and returns its value and error +// Subsequent calls return the cached value and error +func (o *Once) DoValueErr(fn func() (interface{}, error)) (interface{}, error) { + if o.done { + return o.value, o.err + } + defer func() { o.done = true }() + o.value, o.err = fn() + return o.value, o.err +} + +// Reset resets the Once instance to its initial state +// This is mainly useful for testing purposes +func (o *Once) Reset() { + o.done = false + o.err = nil + o.paniced = false + o.value = nil +} + +// IsDone returns whether the Once has been executed +func (o *Once) IsDone() bool { + return o.done +} + +// Error returns the error from the last execution if any +func (o *Once) Error() error { + return o.err +} + +var ( + ErrNotExecuted = errors.New("once: not executed yet") +) + +// Value returns the stored value and an error if not executed yet +func (o *Once) Value() (interface{}, error) { + if !o.done { + return nil, ErrNotExecuted + } + return o.value, nil +} diff --git a/examples/gno.land/p/moul/once/once_test.gno b/examples/gno.land/p/moul/once/once_test.gno new file mode 100644 index 00000000000..7d6114f4a5a --- /dev/null +++ b/examples/gno.land/p/moul/once/once_test.gno @@ -0,0 +1,231 @@ +package once + +import ( + "errors" + "testing" +) + +func TestOnce_Do(t *testing.T) { + counter := 0 + once := New() + + increment := func() { + counter++ + } + + // First call should execute + once.Do(increment) + if counter != 1 { + t.Errorf("expected counter to be 1, got %d", counter) + } + + // Second call should not execute + once.Do(increment) + if counter != 1 { + t.Errorf("expected counter to still be 1, got %d", counter) + } +} + +func TestOnce_DoErr(t *testing.T) { + once := New() + expectedErr := errors.New("test error") + + fn := func() error { + return expectedErr + } + + // First call should return error + if err := once.DoErr(fn); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } + + // Second call should return same error + if err := once.DoErr(fn); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestOnce_DoOrPanic(t *testing.T) { + once := New() + executed := false + + fn := func() { + executed = true + } + + // First call should execute + once.DoOrPanic(fn) + if !executed { + t.Error("function should have executed") + } + + // Second call should panic + defer func() { + if r := recover(); r == nil { + t.Error("expected panic on second execution") + } + }() + once.DoOrPanic(fn) +} + +func TestOnce_DoValue(t *testing.T) { + once := New() + expected := "test value" + counter := 0 + + fn := func() interface{} { + counter++ + return expected + } + + // First call should return value + if result := once.DoValue(fn); result != expected { + t.Errorf("expected %v, got %v", expected, result) + } + + // Second call should return cached value + if result := once.DoValue(fn); result != expected { + t.Errorf("expected %v, got %v", expected, result) + } + + if counter != 1 { + t.Errorf("function should have executed only once, got %d executions", counter) + } +} + +func TestOnce_DoValueErr(t *testing.T) { + once := New() + expectedVal := "test value" + expectedErr := errors.New("test error") + counter := 0 + + fn := func() (interface{}, error) { + counter++ + return expectedVal, expectedErr + } + + // First call should return value and error + val, err := once.DoValueErr(fn) + if val != expectedVal || err != expectedErr { + t.Errorf("expected (%v, %v), got (%v, %v)", expectedVal, expectedErr, val, err) + } + + // Second call should return cached value and error + val, err = once.DoValueErr(fn) + if val != expectedVal || err != expectedErr { + t.Errorf("expected (%v, %v), got (%v, %v)", expectedVal, expectedErr, val, err) + } + + if counter != 1 { + t.Errorf("function should have executed only once, got %d executions", counter) + } +} + +func TestOnce_Reset(t *testing.T) { + once := New() + counter := 0 + + fn := func() { + counter++ + } + + once.Do(fn) + if counter != 1 { + t.Errorf("expected counter to be 1, got %d", counter) + } + + once.Reset() + once.Do(fn) + if counter != 2 { + t.Errorf("expected counter to be 2 after reset, got %d", counter) + } +} + +func TestOnce_IsDone(t *testing.T) { + once := New() + + if once.IsDone() { + t.Error("new Once instance should not be done") + } + + once.Do(func() {}) + + if !once.IsDone() { + t.Error("Once instance should be done after execution") + } +} + +func TestOnce_Error(t *testing.T) { + once := New() + expectedErr := errors.New("test error") + + if err := once.Error(); err != nil { + t.Errorf("expected nil error, got %v", err) + } + + once.DoErr(func() error { + return expectedErr + }) + + if err := once.Error(); err != expectedErr { + t.Errorf("expected error %v, got %v", expectedErr, err) + } +} + +func TestOnce_Value(t *testing.T) { + once := New() + + // Test unexecuted state + val, err := once.Value() + if err != ErrNotExecuted { + t.Errorf("expected ErrNotExecuted, got %v", err) + } + if val != nil { + t.Errorf("expected nil value, got %v", val) + } + + // Test after execution + expected := "test value" + once.DoValue(func() interface{} { + return expected + }) + + val, err = once.Value() + if err != nil { + t.Errorf("expected nil error, got %v", err) + } + if val != expected { + t.Errorf("expected value %v, got %v", expected, val) + } +} + +func TestOnce_DoValueErr_Panic_MarkedDone(t *testing.T) { + once := New() + count := 0 + fn := func() (interface{}, error) { + count++ + panic("panic") + } + var r interface{} + func() { + defer func() { r = recover() }() + once.DoValueErr(fn) + }() + if r == nil { + t.Error("expected panic on first call") + } + if !once.IsDone() { + t.Error("expected once to be marked as done after panic") + } + r = nil + func() { + defer func() { r = recover() }() + once.DoValueErr(fn) + }() + if r != nil { + t.Error("expected no panic on subsequent call") + } + if count != 1 { + t.Errorf("expected count to be 1, got %d", count) + } +} diff --git a/gno.land/pkg/integration/testdata/moul_authz.txtar b/gno.land/pkg/integration/testdata/moul_authz.txtar new file mode 100644 index 00000000000..d0da3f535e8 --- /dev/null +++ b/gno.land/pkg/integration/testdata/moul_authz.txtar @@ -0,0 +1,82 @@ +loadpkg gno.land/p/moul/authz +loadpkg gno.land/r/testing/admin $WORK/admin +loadpkg gno.land/r/testing/resource $WORK/resource + +adduserfrom alice 'smooth crawl poverty trumpet glare useful curtain annual pluck lunar example merge ready forum better verb rescue rule mechanic dynamic drift bench release weekend' +stdout 'g1rfznvu6qfa0sc76cplk5wpqexvefqccjunady0' + +gnoland start + +gnokey maketx call -pkgpath gno.land/r/testing/resource -func Edit -args edited -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice + +gnokey maketx call -pkgpath gno.land/r/testing/admin -func ExecuteAction -args 0 -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice + +gnokey maketx call -pkgpath gno.land/r/testing/resource -func Value -gas-fee 100000ugnot -gas-wanted 2000000 -broadcast -chainid tendermint_test alice +stdout 'edited' + + +-- admin/gno.mod -- +module gno.land/r/testing/admin + +-- admin/admin.gno -- +package admin + +import ( + "std" + "errors" + "gno.land/p/moul/authz" +) + +type prop struct { + title string + action authz.PrivilegedAction +} + +var props []*prop + +func HandlePrivilegedAction(title string, action authz.PrivilegedAction) error { + if std.PreviousRealm().PkgPath() != "gno.land/r/testing/resource" { + return errors.New("unauthorized proposer") + } + props = append(props, &prop{title: title, action: action}) + return nil +} + +func ExecuteAction(index int) { + if std.PreviousRealm().Address() != "g1rfznvu6qfa0sc76cplk5wpqexvefqccjunady0" { + panic(errors.New("not alice")) + } + if err := props[index].action(); err != nil { + panic(err) + } +} + +-- resource/gno.mod -- +module gno.land/r/testing/resource + +-- resource/resource.gno -- +package resource + +import ( + "gno.land/p/moul/authz" + "gno.land/r/testing/admin" +) + +var a *authz.Authorizer = authz.NewWithAuthority(authz.NewContractAuthority("gno.land/r/testing/admin", admin.HandlePrivilegedAction)) + +var value = "init" + +func Value() string { + return value +} + +func Edit(newValue string) { + doEdit := func() error { + value = newValue + return nil + } + if err := a.Do("Edit", doEdit); err != nil { + panic(err) + } +} + From 3288fe82f77afb475796a6ebbe395975ee6f9f0f Mon Sep 17 00:00:00 2001 From: Alexis Colin Date: Tue, 25 Feb 2025 17:25:07 +0900 Subject: [PATCH 8/8] fix: CSP to allow pixel gif from simple analytics (#3822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Related to #3619 This PR updates the Content Security Policy (CSP) to allow Simple Analytics to function correctly. The img-src directive now includes https://sa.gno.services, enabling the tracking pixel to load without being blocked. The tracking pixel (a 1×1 GIF) is essential for Simple Analytics to collect pageview data without requiring JavaScript. When loaded, it sends necessary metadata (e.g., page URL, referrer, user-agent) via the request, allowing privacy-friendly analytics to work. Without this fix, Simple Analytics is unable to capture visits. --- gno.land/cmd/gnoweb/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gno.land/cmd/gnoweb/main.go b/gno.land/cmd/gnoweb/main.go index 0bd38489ff6..8e0b64122a6 100644 --- a/gno.land/cmd/gnoweb/main.go +++ b/gno.land/cmd/gnoweb/main.go @@ -242,7 +242,7 @@ func SecureHeadersMiddleware(next http.Handler, strict bool) http.Handler { // - 'self' allows resources from the same origin. // - 'data:' allows inline images (e.g., base64-encoded images). // - 'https://gnolang.github.io' allows images from this specific domain - used by gno.land. TODO: use a proper generic whitelisted service - w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://sa.gno.services; style-src 'self'; img-src 'self' data: https://gnolang.github.io; font-src 'self'") + w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' https://sa.gno.services; style-src 'self'; img-src 'self' data: https://gnolang.github.io https://sa.gno.services; font-src 'self'") // Enforce HTTPS by telling browsers to only access the site over HTTPS // for a specified duration (1 year in this case). This also applies to