diff --git a/examples/gno.land/p/demo/avl/index/index.gno b/examples/gno.land/p/demo/avl/index/index.gno index 36895bddc6c..97f3725ed03 100644 --- a/examples/gno.land/p/demo/avl/index/index.gno +++ b/examples/gno.land/p/demo/avl/index/index.gno @@ -130,19 +130,26 @@ func (it *IndexedTree) removeFromIndexes(primaryKey string, value interface{}) { } } -// Add these methods to IndexedTree to satisfy avl.TreeInterface -func (it *IndexedTree) Get(key string) (interface{}, bool) { - return it.primary.Get(key) -} - -func (it *IndexedTree) Iterate(start, end string, cb func(key string, value interface{}) bool) bool { - return it.primary.Iterate(start, end, cb) -} - -// Add this method to get access to an index as an avl.TreeInterface func (it *IndexedTree) GetIndexTree(name string) avl.TreeInterface { if idx, exists := it.indexes[name]; exists { return idx.tree } return nil } + +func (it *IndexedTree) GetPrimary() avl.TreeInterface { + return it.primary +} + +func (it *IndexedTree) Update(key string, oldValue interface{}, newValue interface{}) bool { + // Remove old value from indexes + it.removeFromIndexes(key, oldValue) + + // Update primary tree + updated := it.primary.Set(key, newValue) + + // Add new value to indexes + it.addToIndexes(key, newValue) + + return updated +} diff --git a/examples/gno.land/p/demo/avl/index/index_test.gno b/examples/gno.land/p/demo/avl/index/index_test.gno index bf1a76b8108..c46ae2cb4fb 100644 --- a/examples/gno.land/p/demo/avl/index/index_test.gno +++ b/examples/gno.land/p/demo/avl/index/index_test.gno @@ -11,35 +11,183 @@ type Person struct { Age int } -func TestIndexedTree(t *testing.T) { - // Create a new indexed tree - it := NewIndexedTree() +type InvalidPerson struct { + ID string +} + +func TestIndexedTreeComprehensive(t *testing.T) { + // Test 1: Basic operations without any indexes + t.Run("NoIndexes", func(t *testing.T) { + tree := NewIndexedTree() + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + + // Test Set and Get using primary tree directly + tree.Set("1", p1) + val, exists := tree.GetPrimary().Get("1") + if !exists || val.(*Person).Name != "Alice" { + t.Error("Basic Get failed without indexes") + } - // Add secondary indexes - it.AddIndex("name", func(value interface{}) string { - return value.(*Person).Name + // Test direct tree iteration + count := 0 + tree.GetPrimary().Iterate("", "", func(key string, value interface{}) bool { + count++ + return false + }) + if count != 1 { + t.Error("Basic iteration failed") + } }) - it.AddIndex("age", func(value interface{}) string { - return strconv.Itoa(value.(*Person).Age) + // Test 2: Multiple indexes on same field + t.Run("DuplicateIndexes", func(t *testing.T) { + tree := NewIndexedTree() + tree.AddIndex("age1", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) + tree.AddIndex("age2", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) + + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + p2 := &Person{ID: "2", Name: "Bob", Age: 30} + + tree.Set("1", p1) + tree.Set("2", p2) + + // Both indexes should return the same results + results1 := tree.GetByIndex("age1", "30") + results2 := tree.GetByIndex("age2", "30") + + if len(results1) != 2 || len(results2) != 2 { + t.Error("Duplicate indexes returned different results") + } }) - // Add some test data - person1 := &Person{ID: "1", Name: "Alice", Age: 30} - person2 := &Person{ID: "2", Name: "Bob", Age: 30} + // Test 3: Invalid extractor + t.Run("InvalidExtractor", func(t *testing.T) { + didPanic := false + + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + tree := NewIndexedTree() + tree.AddIndex("name", func(v interface{}) string { + // This should panic when trying to use an InvalidPerson + return v.(*Person).Name // Intentionally wrong type assertion + }) + + invalid := &InvalidPerson{ID: "1"} + tree.Set("1", invalid) // This should trigger the panic + }() + + if !didPanic { + t.Error("Expected panic from invalid type") + } + }) + + // Test 4: Mixed usage of indexed and direct access + t.Run("MixedUsage", func(t *testing.T) { + tree := NewIndexedTree() + tree.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) + + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + + // Use Set instead of direct tree access to ensure indexes are updated + tree.Set("1", p1) + + // Index should work + results := tree.GetByIndex("age", "30") + if len(results) != 1 { + t.Error("Index failed after direct tree usage") + } + }) - it.Set(person1.ID, person1) - it.Set(person2.ID, person2) + // Test 5: Using index as TreeInterface + t.Run("IndexAsTreeInterface", func(t *testing.T) { + tree := NewIndexedTree() + tree.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) - // Query by name index - aliceResults := it.GetByIndex("name", "Alice") - if len(aliceResults) != 1 { - t.Error("Expected 1 result for Alice") - } + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + p2 := &Person{ID: "2", Name: "Bob", Age: 30} - // Query by age index - age30Results := it.GetByIndex("age", "30") - if len(age30Results) != 2 { - t.Error("Expected 2 results for age 30") - } + tree.Set("1", p1) + tree.Set("2", p2) + + // Get the index as TreeInterface + ageIndex := tree.GetIndexTree("age") + if ageIndex == nil { + t.Error("Failed to get index as TreeInterface") + } + + // Use the interface methods + val, exists := ageIndex.Get("30") + if !exists { + t.Error("Failed to get value through index interface") + } + + // The value should be a []string of primary keys + primaryKeys := val.([]string) + if len(primaryKeys) != 2 { + t.Error("Wrong number of primary keys in index") + } + }) + + // Test 6: Remove operations + t.Run("RemoveOperations", func(t *testing.T) { + tree := NewIndexedTree() + tree.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) + + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + tree.Set("1", p1) + + // Remove and verify both primary and index + tree.Remove("1") + + if _, exists := tree.GetPrimary().Get("1"); exists { + t.Error("Entry still exists in primary after remove") + } + + results := tree.GetByIndex("age", "30") + if len(results) != 0 { + t.Error("Entry still exists in index after remove") + } + }) + + // Test 7: Update operations + t.Run("UpdateOperations", func(t *testing.T) { + tree := NewIndexedTree() + tree.AddIndex("age", func(v interface{}) string { + return strconv.Itoa(v.(*Person).Age) + }) + + p1 := &Person{ID: "1", Name: "Alice", Age: 30} + tree.Set("1", p1) + + // Update age using the new Update method + p1New := &Person{ID: "1", Name: "Alice", Age: 31} + tree.Update("1", p1, p1New) + + // Check old index is removed + results30 := tree.GetByIndex("age", "30") + if len(results30) != 0 { + t.Error("Old index entry still exists") + } + + // Check new index is added + results31 := tree.GetByIndex("age", "31") + if len(results31) != 1 { + t.Error("New index entry not found") + } + }) } diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index cccdc0df645..d229ab17d5e 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -11,7 +11,7 @@ import ( // Pager is a struct that holds the AVL tree and pagination parameters. type Pager struct { - Tree *avl.Tree + Tree avl.ITree PageQueryParam string SizeQueryParam string DefaultPageSize int @@ -37,7 +37,7 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager { +func NewPager(tree avl.TreeInterface, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", diff --git a/examples/gno.land/p/demo/avl/rotree/gno.mod b/examples/gno.land/p/demo/avl/rotree/gno.mod new file mode 100644 index 00000000000..d2cb439b2eb --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/rotree diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno new file mode 100644 index 00000000000..3e093c4d0e0 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -0,0 +1,162 @@ +// Package rotree provides a read-only wrapper for avl.Tree with safe value transformation. +// +// It is useful when you want to expose a read-only view of a tree while ensuring that +// the sensitive data cannot be modified. +// +// Example: +// +// // Define a user structure with sensitive data +// type User struct { +// Name string +// Balance int +// Internal string // sensitive field +// } +// +// // Create and populate the original tree +// privateTree := avl.NewTree() +// privateTree.Set("alice", &User{ +// Name: "Alice", +// Balance: 100, +// Internal: "sensitive", +// }) +// +// // Create a safe transformation function that copies the struct +// // while excluding sensitive data +// makeEntrySafeFn := func(v interface{}) interface{} { +// u := v.(*User) +// return &User{ +// Name: u.Name, +// Balance: u.Balance, +// Internal: "", // omit sensitive data +// } +// } +// +// // Create a read-only view of the tree +// PublicTree := rotree.Wrap(tree, makeEntrySafeFn) +// +// // Safely access the data +// value, _ := roTree.Get("alice") +// user := value.(*User) +// // user.Name == "Alice" +// // user.Balance == 100 +// // user.Internal == "" (sensitive data is filtered) +package rotree + +import ( + "gno.land/p/demo/avl" +) + +// Wrap creates a new ReadOnlyTree from an existing avl.Tree and a safety transformation function. +// If makeEntrySafeFn is nil, values will be returned as-is without transformation. +// +// makeEntrySafeFn is a function that transforms a tree entry into a safe version that can be exposed to external users. +// This function should be implemented based on the specific safety requirements of your use case: +// +// 1. No-op transformation: For primitive types (int, string, etc.) or already safe objects, +// simply pass nil as the makeEntrySafeFn to return values as-is. +// +// 2. Defensive copying: For mutable types like slices or maps, you should create a deep copy +// to prevent modification of the original data. +// Example: func(v interface{}) interface{} { return append([]int{}, v.([]int)...) } +// +// 3. Read-only wrapper: Return a read-only version of the object that implements +// a limited interface. +// Example: func(v interface{}) interface{} { return NewReadOnlyObject(v) } +// +// 4. DAO transformation: Transform the object into a data access object that +// controls how the underlying data can be accessed. +// Example: func(v interface{}) interface{} { return NewDAO(v) } +// +// The function ensures that the returned object is safe to expose to untrusted code, +// preventing unauthorized modifications to the original data structure. +func Wrap(tree *avl.Tree, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyTree { + return &ReadOnlyTree{ + tree: tree, + makeEntrySafeFn: makeEntrySafeFn, + } +} + +// ReadOnlyTree wraps an avl.Tree and provides read-only access. +type ReadOnlyTree struct { + tree *avl.Tree + makeEntrySafeFn func(interface{}) interface{} +} + +// Verify that ReadOnlyTree implements ITree +var _ avl.ITree = (*ReadOnlyTree)(nil) + +// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value +func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { + if roTree.makeEntrySafeFn == nil { + return value + } + return roTree.makeEntrySafeFn(value) +} + +// Size returns the number of key-value pairs in the tree. +func (roTree *ReadOnlyTree) Size() int { + return roTree.tree.Size() +} + +// Has checks whether a key exists in the tree. +func (roTree *ReadOnlyTree) Has(key string) bool { + return roTree.tree.Has(key) +} + +// Get retrieves the value associated with the given key, converted to a safe format. +func (roTree *ReadOnlyTree) Get(key string) (interface{}, bool) { + value, exists := roTree.tree.Get(key) + if !exists { + return nil, false + } + return roTree.getSafeValue(value), true +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree, with the value converted to a safe format. +func (roTree *ReadOnlyTree) GetByIndex(index int) (string, interface{}) { + key, value := roTree.tree.GetByIndex(index) + return key, roTree.getSafeValue(value) +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) Iterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.Iterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) ReverseIterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) IterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.IterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// Set is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Set(key string, value interface{}) bool { + panic("Set operation not supported on ReadOnlyTree") +} + +// Remove is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Remove(key string) (value interface{}, removed bool) { + panic("Remove operation not supported on ReadOnlyTree") +} + +// RemoveByIndex is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) RemoveByIndex(index int) (key string, value interface{}) { + panic("RemoveByIndex operation not supported on ReadOnlyTree") +} diff --git a/examples/gno.land/p/demo/avl/rotree/rotree_test.gno b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno new file mode 100644 index 00000000000..fbc14bd688d --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno @@ -0,0 +1,222 @@ +package rotree + +import ( + "testing" + + "gno.land/p/demo/avl" +) + +func TestExample(t *testing.T) { + // User represents our internal data structure + type User struct { + ID string + Name string + Balance int + Internal string // sensitive internal data + } + + // Create and populate the original tree with user pointers + tree := avl.NewTree() + tree.Set("alice", &User{ + ID: "1", + Name: "Alice", + Balance: 100, + Internal: "sensitive_data_1", + }) + tree.Set("bob", &User{ + ID: "2", + Name: "Bob", + Balance: 200, + Internal: "sensitive_data_2", + }) + + // Define a makeEntrySafeFn that: + // 1. Creates a defensive copy of the User struct + // 2. Omits sensitive internal data + makeEntrySafeFn := func(v interface{}) interface{} { + originalUser := v.(*User) + return &User{ + ID: originalUser.ID, + Name: originalUser.Name, + Balance: originalUser.Balance, + Internal: "", // Omit sensitive data + } + } + + // Create a read-only view of the tree + roTree := Wrap(tree, makeEntrySafeFn) + + // Test retrieving and verifying a user + t.Run("Get User", func(t *testing.T) { + // Get user from read-only tree + value, exists := roTree.Get("alice") + if !exists { + t.Fatal("User 'alice' not found") + } + + user := value.(*User) + + // Verify user data is correct + if user.Name != "Alice" || user.Balance != 100 { + t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) + } + + // Verify sensitive data is not exposed + if user.Internal != "" { + t.Error("Sensitive data should not be exposed") + } + + // Verify it's a different instance than the original + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if user == originalUser { + t.Error("Read-only tree should return a copy, not the original pointer") + } + }) + + // Test iterating over users + t.Run("Iterate Users", func(t *testing.T) { + count := 0 + roTree.Iterate("", "", func(key string, value interface{}) bool { + user := value.(*User) + // Verify each user has empty Internal field + if user.Internal != "" { + t.Error("Sensitive data exposed during iteration") + } + count++ + return false + }) + + if count != 2 { + t.Errorf("Expected 2 users, got %d", count) + } + }) + + // Verify that modifications to the returned user don't affect the original + t.Run("Modification Safety", func(t *testing.T) { + value, _ := roTree.Get("alice") + user := value.(*User) + + // Try to modify the returned user + user.Balance = 999 + user.Internal = "hacked" + + // Verify original is unchanged + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if originalUser.Balance != 100 || originalUser.Internal != "sensitive_data_1" { + t.Error("Original user data was modified") + } + }) +} + +func TestReadOnlyTree(t *testing.T) { + // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation + makeEntrySafeFn := func(value interface{}) interface{} { + return value.(string) + "_readonly" + } + + tree := avl.NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + roTree := Wrap(tree, makeEntrySafeFn) + + tests := []struct { + name string + key string + expected interface{} + exists bool + }{ + {"ExistingKey1", "key1", "value1_readonly", true}, + {"ExistingKey2", "key2", "value2_readonly", true}, + {"NonExistingKey", "key4", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, exists := roTree.Get(tt.key) + if exists != tt.exists || value != tt.expected { + t.Errorf("For key %s, expected %v (exists: %v), got %v (exists: %v)", tt.key, tt.expected, tt.exists, value, exists) + } + }) + } +} + +// Add example tests showing different makeEntrySafeFn implementations +func TestMakeEntrySafeFnVariants(t *testing.T) { + tree := avl.NewTree() + tree.Set("slice", []int{1, 2, 3}) + tree.Set("map", map[string]int{"a": 1}) + + tests := []struct { + name string + makeEntrySafeFn func(interface{}) interface{} + key string + validate func(t *testing.T, value interface{}) + }{ + { + name: "Defensive Copy Slice", + makeEntrySafeFn: func(v interface{}) interface{} { + original := v.([]int) + return append([]int{}, original...) + }, + key: "slice", + validate: func(t *testing.T, value interface{}) { + slice := value.([]int) + // Modify the returned slice + slice[0] = 999 + // Verify original is unchanged + originalValue, _ := tree.Get("slice") + original := originalValue.([]int) + if original[0] != 1 { + t.Error("Original slice was modified") + } + }, + }, + // Add more test cases for different makeEntrySafeFn implementations + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + roTree := Wrap(tree, tt.makeEntrySafeFn) + value, exists := roTree.Get(tt.key) + if !exists { + t.Fatal("Key not found") + } + tt.validate(t, value) + }) + } +} + +func TestNilMakeEntrySafeFn(t *testing.T) { + // Create a tree with some test data + tree := avl.NewTree() + originalValue := []int{1, 2, 3} + tree.Set("test", originalValue) + + // Create a ReadOnlyTree with nil makeEntrySafeFn + roTree := Wrap(tree, nil) + + // Test that we get back the original value + value, exists := roTree.Get("test") + if !exists { + t.Fatal("Key not found") + } + + // Verify it's the exact same slice (not a copy) + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference") + } + + // Test through iteration as well + roTree.Iterate("", "", func(key string, value interface{}) bool { + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference in iteration") + } + return false + }) +} diff --git a/examples/gno.land/p/demo/avl/tree.gno b/examples/gno.land/p/demo/avl/tree.gno index e7aa55eb7e4..3834246d2cd 100644 --- a/examples/gno.land/p/demo/avl/tree.gno +++ b/examples/gno.land/p/demo/avl/tree.gno @@ -1,5 +1,23 @@ package avl +type ITree interface { + // read operations + + Size() int + Has(key string) bool + Get(key string) (value interface{}, exists bool) + GetByIndex(index int) (key string, value interface{}) + Iterate(start, end string, cb IterCbFn) bool + ReverseIterate(start, end string, cb IterCbFn) bool + IterateByOffset(offset int, count int, cb IterCbFn) bool + ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool + + // write operations + + Set(key string, value interface{}) (updated bool) + Remove(key string) (value interface{}, removed bool) +} + type IterCbFn func(key string, value interface{}) bool //---------------------------------------- @@ -101,3 +119,6 @@ func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) boo }, ) } + +// Verify that Tree implements TreeInterface +var _ ITree = (*Tree)(nil) diff --git a/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar new file mode 100644 index 00000000000..56050f4733b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar @@ -0,0 +1,57 @@ +# ensure users get proper out of gas errors when they add packages + +# start a new node +gnoland start + +# add foo package +gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1 + + +# add bar package +# out of gas at store.GetPackage() with gas 60000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1 + +# Out of gas error + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + + +# out of gas at store.store.GetTypeSafe() with gas 63000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1 + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + +-- foo/foo.gno -- +package foo + +type Counter int + +func Inc(i Counter) Counter{ + i = i+1 + return i +} + +-- bar/bar.gno -- +package bar + +import "gno.land/r/foo" + +type NewCounter foo.Counter + +func Add2(i NewCounter) NewCounter{ + i=i+2 + + return i +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a3e498710bb..15f268f6321 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "github.com/gnolang/gno/tm2/pkg/errors" + tmstore "github.com/gnolang/gno/tm2/pkg/store" ) const ( @@ -365,6 +366,12 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { func doRecover(stack []BlockNode, n Node) { if r := recover(); r != nil { + // Catch the out-of-gas exception and throw it + if exp, ok := r.(tmstore.OutOfGasException); ok { + exp.Descriptor = fmt.Sprintf("in preprocess: %v", r) + panic(exp) + } + if _, ok := r.(*PreprocessError); ok { // re-panic directly if this is a PreprocessError already. panic(r)