From d51210a9e672f83ff30fc8a5a3472692f239cff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 13 Mar 2026 14:20:52 -0700 Subject: [PATCH 1/3] add Account.Storage.limitedToPaths, which returns a new Account.Storage limited to given paths --- bbq/vm/value_account_storage.go | 16 +- interpreter/account_test.go | 376 +++++++++++++++++++++++++++ interpreter/errors.go | 24 ++ interpreter/interpreter.go | 97 ++++++- interpreter/value_account_storage.go | 115 +++++++- sema/account.cdc | 6 + sema/account.gen.go | 41 +++ stdlib/account.go | 1 + 8 files changed, 648 insertions(+), 28 deletions(-) diff --git a/bbq/vm/value_account_storage.go b/bbq/vm/value_account_storage.go index bb9c82a306..bd0c5e7e6a 100644 --- a/bbq/vm/value_account_storage.go +++ b/bbq/vm/value_account_storage.go @@ -37,7 +37,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeSaveFunctionName, sema.Account_StorageTypeSaveFunctionType, - interpreter.NativeAccountStorageSaveFunction(nil), + interpreter.NativeAccountStorageSaveFunction(nil, nil), ), ) @@ -47,7 +47,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeBorrowFunctionName, sema.Account_StorageTypeBorrowFunctionType, - interpreter.NativeAccountStorageBorrowFunction(nil), + interpreter.NativeAccountStorageBorrowFunction(nil, nil), ), ) @@ -57,7 +57,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeForEachPublicFunctionName, sema.Account_StorageTypeForEachPublicFunctionType, - interpreter.NativeAccountStorageIterateFunction(nil, common.PathDomainPublic, sema.PublicPathType), + interpreter.NativeAccountStorageIterateFunction(nil, common.PathDomainPublic, sema.PublicPathType, nil), ), ) @@ -67,7 +67,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeForEachStoredFunctionName, sema.Account_StorageTypeForEachPublicFunctionType, - interpreter.NativeAccountStorageIterateFunction(nil, common.PathDomainStorage, sema.StoragePathType), + interpreter.NativeAccountStorageIterateFunction(nil, common.PathDomainStorage, sema.StoragePathType, nil), ), ) @@ -77,7 +77,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeTypeFunctionName, sema.Account_StorageTypeTypeFunctionType, - interpreter.NativeAccountStorageTypeFunction(nil), + interpreter.NativeAccountStorageTypeFunction(nil, nil), ), ) @@ -87,7 +87,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeLoadFunctionName, sema.Account_StorageTypeLoadFunctionType, - interpreter.NativeAccountStorageLoadFunction(nil), + interpreter.NativeAccountStorageLoadFunction(nil, nil), ), ) @@ -97,7 +97,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeCopyFunctionName, sema.Account_StorageTypeCopyFunctionType, - interpreter.NativeAccountStorageCopyFunction(nil), + interpreter.NativeAccountStorageCopyFunction(nil, nil), ), ) @@ -107,7 +107,7 @@ func init() { NewNativeFunctionValue( sema.Account_StorageTypeCheckFunctionName, sema.Account_StorageTypeCheckFunctionType, - interpreter.NativeAccountStorageCheckFunction(nil), + interpreter.NativeAccountStorageCheckFunction(nil, nil), ), ) } diff --git a/interpreter/account_test.go b/interpreter/account_test.go index eaf93b339b..1f7e2f6411 100644 --- a/interpreter/account_test.go +++ b/interpreter/account_test.go @@ -1698,3 +1698,379 @@ func TestInterpretAccountStorageReadFunctionTypes(t *testing.T) { require.NoError(t, err) require.Equal(t, interpreter.FalseValue, areEqual) } + +func TestInterpretAccountStorageLimitedToPaths(t *testing.T) { + + t.Parallel() + + t.Run("load allowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S { + let n: Int + init(n: Int) { self.n = n } + } + + fun setup() { + account.storage.save(S(n: 1), to: /storage/a) + account.storage.save(S(n: 2), to: /storage/b) + } + + fun test(): Int? { + let limited = account.storage.limitedToPaths([/storage/a]) + let s = limited.load(from: /storage/a) + return s?.n + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + + require.IsType(t, &interpreter.SomeValue{}, value) + innerValue := value.(*interpreter.SomeValue).InnerValue() + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(1), innerValue) + }) + + t.Run("load disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S { + let n: Int + init(n: Int) { self.n = n } + } + + fun setup() { + account.storage.save(S(n: 1), to: /storage/a) + account.storage.save(S(n: 2), to: /storage/b) + } + + fun test(): Int? { + let limited = account.storage.limitedToPaths([/storage/a]) + let s = limited.load(from: /storage/b) + return s?.n + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.Nil, value) + }) + + t.Run("save disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun test() { + let limited = account.storage.limitedToPaths([/storage/a]) + limited.save(S(), to: /storage/b) + } + `, sema.Config{}) + + _, err := inter.Invoke("test") + RequireError(t, err) + + var pathNotAllowedError *interpreter.PathNotAllowedError + require.ErrorAs(t, err, &pathNotAllowedError) + }) + + t.Run("save allowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, getAccountValues, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun test() { + let limited = account.storage.limitedToPaths([/storage/a]) + limited.save(S(), to: /storage/a) + } + `, sema.Config{}) + + _, err := inter.Invoke("test") + require.NoError(t, err) + require.Len(t, getAccountValues(), 1) + }) + + t.Run("borrow disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S { + let n: Int + init(n: Int) { self.n = n } + } + + fun setup() { + account.storage.save(S(n: 1), to: /storage/a) + } + + fun test(): Int? { + let limited = account.storage.limitedToPaths([/storage/b]) + let ref = limited.borrow<&S>(from: /storage/a) + return ref?.n + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.Nil, value) + }) + + t.Run("copy disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S { + let n: Int + init(n: Int) { self.n = n } + } + + fun setup() { + account.storage.save(S(n: 1), to: /storage/a) + } + + fun test(): Int? { + let limited = account.storage.limitedToPaths([/storage/b]) + let s = limited.copy(from: /storage/a) + return s?.n + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.Nil, value) + }) + + t.Run("check disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + } + + fun test(): Bool { + let limited = account.storage.limitedToPaths([/storage/b]) + return limited.check(from: /storage/a) + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.FalseValue, value) + }) + + t.Run("check allowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + } + + fun test(): Bool { + let limited = account.storage.limitedToPaths([/storage/a]) + return limited.check(from: /storage/a) + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.TrueValue, value) + }) + + t.Run("type disallowed", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + } + + fun test(): Type? { + let limited = account.storage.limitedToPaths([/storage/b]) + return limited.type(at: /storage/a) + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.Nil, value) + }) + + t.Run("forEachStored limited", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + account.storage.save(S(), to: /storage/b) + account.storage.save(S(), to: /storage/c) + } + + fun test(): Int { + let limited = account.storage.limitedToPaths([/storage/a, /storage/c]) + var count = 0 + limited.forEachStored(fun (path: StoragePath, type: Type): Bool { + count = count + 1 + return true + }) + return count + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(2), value) + }) + + t.Run("nested limitedToPaths (intersection)", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + account.storage.save(S(), to: /storage/b) + account.storage.save(S(), to: /storage/c) + } + + fun test(): Int { + let limited = account.storage.limitedToPaths([/storage/a, /storage/b, /storage/c]) + let nested = limited.limitedToPaths([/storage/b]) + var count = 0 + nested.forEachStored(fun (path: StoragePath, type: Type): Bool { + count = count + 1 + return true + }) + return count + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + value, err := inter.Invoke("test") + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(1), value) + }) + + t.Run("empty paths blocks all", func(t *testing.T) { + t.Parallel() + + address := interpreter.NewUnmeteredAddressValueFromBytes([]byte{42}) + + inter, _, _ := testAccount(t, address, true, nil, ` + struct S {} + + fun setup() { + account.storage.save(S(), to: /storage/a) + account.storage.save(S(), to: /storage/b) + } + + fun testLoad(): S? { + let limited = account.storage.limitedToPaths([]) + return limited.load(from: /storage/a) + } + + fun testCheck(): Bool { + let limited = account.storage.limitedToPaths([]) + return limited.check(from: /storage/a) + } + + fun testForEach(): Int { + let limited = account.storage.limitedToPaths([]) + var count = 0 + limited.forEachStored(fun (path: StoragePath, type: Type): Bool { + count = count + 1 + return true + }) + return count + } + + fun testSave() { + let limited = account.storage.limitedToPaths([]) + limited.save(S(), to: /storage/c) + } + `, sema.Config{}) + + _, err := inter.Invoke("setup") + require.NoError(t, err) + + loadResult, err := inter.Invoke("testLoad") + require.NoError(t, err) + require.Equal(t, interpreter.Nil, loadResult) + + checkResult, err := inter.Invoke("testCheck") + require.NoError(t, err) + require.Equal(t, interpreter.FalseValue, checkResult) + + forEachResult, err := inter.Invoke("testForEach") + require.NoError(t, err) + require.Equal(t, interpreter.NewUnmeteredIntValueFromInt64(0), forEachResult) + + _, err = inter.Invoke("testSave") + RequireError(t, err) + + var pathNotAllowedError *interpreter.PathNotAllowedError + require.ErrorAs(t, err, &pathNotAllowedError) + }) +} diff --git a/interpreter/errors.go b/interpreter/errors.go index 9790b42567..b3012fb2fa 100644 --- a/interpreter/errors.go +++ b/interpreter/errors.go @@ -567,6 +567,30 @@ func (e *OverwriteError) SetLocationRange(locationRange LocationRange) { e.LocationRange = locationRange } +// PathNotAllowedError +type PathNotAllowedError struct { + LocationRange + Path PathValue + Address AddressValue +} + +var _ errors.UserError = &PathNotAllowedError{} +var _ HasLocationRange = &PathNotAllowedError{} + +func (*PathNotAllowedError) IsUserError() {} + +func (e *PathNotAllowedError) Error() string { + return fmt.Sprintf( + "path %s in account %s is not allowed in limited storage", + e.Path, + e.Address, + ) +} + +func (e *PathNotAllowedError) SetLocationRange(locationRange LocationRange) { + e.LocationRange = locationRange +} + // ArrayIndexOutOfBoundsError type ArrayIndexOutOfBoundsError struct { LocationRange diff --git a/interpreter/interpreter.go b/interpreter/interpreter.go index cbf3a962cf..65b8463087 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -4620,7 +4620,7 @@ func IsSubTypeOfSemaType(typeConverter TypeConverter, staticSubType StaticType, return sema.IsSubType(semaSubType, superType) } -func domainPaths(context StorageContext, address common.Address, domain common.PathDomain) []Value { +func domainPaths(context StorageContext, address common.Address, domain common.PathDomain, allowedPaths map[PathValue]struct{}) []Value { storageMap := context.Storage().GetDomainStorageMap(context, address, domain.StorageDomain(), false) if storageMap == nil { return []Value{} @@ -4635,6 +4635,9 @@ func domainPaths(context StorageContext, address common.Address, domain common.P // TODO: unfortunately, the iterator only returns an atree.Value, not a StorageMapKey identifier := string(key.(StringAtreeValue)) path := NewPathValue(context, domain, identifier) + if !isPathAllowed(path, allowedPaths) { + continue + } paths = append(paths, path) } } @@ -4646,9 +4649,10 @@ func accountPaths( addressValue AddressValue, domain common.PathDomain, pathType StaticType, + allowedPaths map[PathValue]struct{}, ) *ArrayValue { address := addressValue.ToAddress() - values := domainPaths(context, address, domain) + values := domainPaths(context, address, domain, allowedPaths) return NewArrayValue( context, NewVariableSizedStaticType(context, pathType), @@ -4660,24 +4664,28 @@ func accountPaths( func publicAccountPaths( context ArrayCreationContext, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) *ArrayValue { return accountPaths( context, addressValue, common.PathDomainPublic, PrimitiveStaticTypePublicPath, + allowedPaths, ) } func storageAccountPaths( context ArrayCreationContext, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) *ArrayValue { return accountPaths( context, addressValue, common.PathDomainStorage, PrimitiveStaticTypeStoragePath, + allowedPaths, ) } @@ -4691,6 +4699,7 @@ func NativeAccountStorageIterateFunction( addressPointer *AddressValue, domain common.PathDomain, pathType sema.Type, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -4707,6 +4716,7 @@ func NativeAccountStorageIterateFunction( address, domain, pathType, + allowedPaths, ) } } @@ -4718,13 +4728,14 @@ func newStorageIterationFunction( addressValue AddressValue, domain common.PathDomain, pathType sema.Type, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, functionType, - NativeAccountStorageIterateFunction(&addressValue, domain, pathType), + NativeAccountStorageIterateFunction(&addressValue, domain, pathType, allowedPaths), ) } @@ -4734,6 +4745,7 @@ func AccountStorageIterate( address common.Address, domain common.PathDomain, pathType sema.Type, + allowedPaths map[PathValue]struct{}, ) Value { fn, ok := arguments[0].(FunctionValue) if !ok { @@ -4754,6 +4766,15 @@ func AccountStorageIterate( for key, value := storageIterator.Next(invocationContext); key != nil && value != nil; key, value = storageIterator.Next(invocationContext) { + // TODO: unfortunately, the iterator only returns an atree.Value, not a StorageMapKey + identifier := string(key.(StringAtreeValue)) + pathValue := NewPathValue(invocationContext, domain, identifier) + + // Skip paths not in the allowed set + if !isPathAllowed(pathValue, allowedPaths) { + continue + } + staticType := value.StaticType(invocationContext) // Perform a forced value de-referencing to see if the associated type is not broken. @@ -4768,9 +4789,6 @@ func AccountStorageIterate( continue } - // TODO: unfortunately, the iterator only returns an atree.Value, not a StorageMapKey - identifier := string(key.(StringAtreeValue)) - pathValue := NewPathValue(invocationContext, domain, identifier) runtimeType := NewTypeValue(invocationContext, staticType) arguments := []Value{pathValue, runtimeType} @@ -4909,6 +4927,7 @@ func checkValue( func NativeAccountStorageSaveFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -4923,6 +4942,7 @@ func NativeAccountStorageSaveFunction( context, args, addressValue, + allowedPaths, ) } } @@ -4931,13 +4951,14 @@ func authAccountStorageSaveFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, sema.Account_StorageTypeSaveFunctionType, - NativeAccountStorageSaveFunction(&addressValue), + NativeAccountStorageSaveFunction(&addressValue, allowedPaths), ) } @@ -4945,6 +4966,7 @@ func AccountStorageSave( context InvocationContext, arguments []Value, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) Value { value := arguments[0] @@ -4953,6 +4975,13 @@ func AccountStorageSave( panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + panic(&PathNotAllowedError{ + Address: addressValue, + Path: path, + }) + } + domain := path.Domain.StorageDomain() identifier := path.Identifier @@ -4992,6 +5021,7 @@ func AccountStorageSave( func NativeAccountStorageTypeFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -5006,6 +5036,7 @@ func NativeAccountStorageTypeFunction( context, args, address, + allowedPaths, ) } } @@ -5014,13 +5045,14 @@ func authAccountStorageTypeFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, sema.Account_StorageTypeTypeFunctionType, - NativeAccountStorageTypeFunction(&addressValue), + NativeAccountStorageTypeFunction(&addressValue, allowedPaths), ) } @@ -5028,12 +5060,17 @@ func AccountStorageType( interpreter InvocationContext, arguments []Value, address common.Address, + allowedPaths map[PathValue]struct{}, ) Value { path, ok := arguments[0].(PathValue) if !ok { panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + return Nil + } + domain := path.Domain.StorageDomain() identifier := path.Identifier @@ -5058,12 +5095,14 @@ func authAccountStorageLoadFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return authAccountLoadFunction( context, storageValue, addressValue, sema.Account_StorageTypeLoadFunctionType, + allowedPaths, ) } @@ -5071,17 +5110,20 @@ func authAccountStorageCopyFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return authAccountCopyFunction( context, storageValue, addressValue, sema.Account_StorageTypeCopyFunctionType, + allowedPaths, ) } func NativeAccountStorageCopyFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -5098,12 +5140,14 @@ func NativeAccountStorageCopyFunction( args, semaBorrowType, address, + allowedPaths, ) } } func NativeAccountStorageLoadFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -5120,6 +5164,7 @@ func NativeAccountStorageLoadFunction( args, semaBorrowType, address, + allowedPaths, ) } } @@ -5129,13 +5174,14 @@ func authAccountCopyFunction( storageValue *SimpleCompositeValue, addressValue AddressValue, functionType *sema.FunctionType, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, functionType, - NativeAccountStorageCopyFunction(&addressValue), + NativeAccountStorageCopyFunction(&addressValue, allowedPaths), ) } @@ -5144,13 +5190,14 @@ func authAccountLoadFunction( storageValue *SimpleCompositeValue, addressValue AddressValue, functionType *sema.FunctionType, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, functionType, - NativeAccountStorageLoadFunction(&addressValue), + NativeAccountStorageLoadFunction(&addressValue, allowedPaths), ) } @@ -5159,12 +5206,17 @@ func AccountStorageCopy( arguments []Value, typeParameter sema.Type, address common.Address, + allowedPaths map[PathValue]struct{}, ) Value { path, ok := arguments[0].(PathValue) if !ok { panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + return Nil + } + domain := path.Domain.StorageDomain() identifier := path.Identifier @@ -5207,12 +5259,17 @@ func AccountStorageLoad( arguments []Value, typeParameter sema.Type, address common.Address, + allowedPaths map[PathValue]struct{}, ) Value { path, ok := arguments[0].(PathValue) if !ok { panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + return Nil + } + domain := path.Domain.StorageDomain() identifier := path.Identifier @@ -5253,6 +5310,7 @@ func AccountStorageLoad( func NativeAccountStorageBorrowFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -5269,6 +5327,7 @@ func NativeAccountStorageBorrowFunction( args, typeParameter, address, + allowedPaths, ) } } @@ -5277,13 +5336,14 @@ func authAccountStorageBorrowFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, sema.Account_StorageTypeBorrowFunctionType, - NativeAccountStorageBorrowFunction(&addressValue), + NativeAccountStorageBorrowFunction(&addressValue, allowedPaths), ) } @@ -5292,12 +5352,17 @@ func AccountStorageBorrow( arguments []Value, typeParameter sema.Type, address common.Address, + allowedPaths map[PathValue]struct{}, ) Value { path, ok := arguments[0].(PathValue) if !ok { panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + return Nil + } + referenceType, ok := typeParameter.(*sema.ReferenceType) if !ok { panic(errors.NewUnreachableError()) @@ -5328,6 +5393,7 @@ func AccountStorageBorrow( func NativeAccountStorageCheckFunction( addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, ) NativeFunction { return func( context NativeFunctionContext, @@ -5344,6 +5410,7 @@ func NativeAccountStorageCheckFunction( address, args, typeArgument, + allowedPaths, ) } } @@ -5352,13 +5419,14 @@ func authAccountStorageCheckFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, addressValue AddressValue, + allowedPaths map[PathValue]struct{}, ) BoundFunctionValue { return NewBoundHostFunctionValue( context, storageValue, sema.Account_StorageTypeCheckFunctionType, - NativeAccountStorageCheckFunction(&addressValue), + NativeAccountStorageCheckFunction(&addressValue, allowedPaths), ) } @@ -5367,12 +5435,17 @@ func AccountStorageCheck( address common.Address, arguments []Value, typeParameter sema.Type, + allowedPaths map[PathValue]struct{}, ) Value { path, ok := arguments[0].(PathValue) if !ok { panic(errors.NewUnreachableError()) } + if !isPathAllowed(path, allowedPaths) { + return FalseValue + } + domain := path.Domain.StorageDomain() identifier := path.Identifier diff --git a/interpreter/value_account_storage.go b/interpreter/value_account_storage.go index 3d958ea001..167065994f 100644 --- a/interpreter/value_account_storage.go +++ b/interpreter/value_account_storage.go @@ -22,6 +22,7 @@ import ( "fmt" "github.com/onflow/cadence/common" + "github.com/onflow/cadence/errors" "github.com/onflow/cadence/sema" ) @@ -32,11 +33,14 @@ var account_StorageStaticType StaticType = PrimitiveStaticTypeAccount_Storage var account_StorageFieldNames []string = nil // NewAccountStorageValue constructs an Account.Storage value. +// When allowedPaths is nil, all paths are accessible (unlimited storage). +// When allowedPaths is non-nil, only the specified paths are accessible. func NewAccountStorageValue( gauge common.MemoryGauge, address AddressValue, storageUsedGet func(context MemberAccessibleContext) UInt64Value, storageCapacityGet func(context MemberAccessibleContext) UInt64Value, + allowedPaths map[PathValue]struct{}, ) Value { var storageValue *SimpleCompositeValue @@ -53,6 +57,7 @@ func NewAccountStorageValue( address, common.PathDomainPublic, sema.PublicPathType, + allowedPaths, ) case sema.Account_StorageTypeForEachStoredFunctionName: @@ -63,25 +68,36 @@ func NewAccountStorageValue( address, common.PathDomainStorage, sema.StoragePathType, + allowedPaths, ) case sema.Account_StorageTypeTypeFunctionName: - return authAccountStorageTypeFunction(context, storageValue, address) + return authAccountStorageTypeFunction(context, storageValue, address, allowedPaths) case sema.Account_StorageTypeLoadFunctionName: - return authAccountStorageLoadFunction(context, storageValue, address) + return authAccountStorageLoadFunction(context, storageValue, address, allowedPaths) case sema.Account_StorageTypeCopyFunctionName: - return authAccountStorageCopyFunction(context, storageValue, address) + return authAccountStorageCopyFunction(context, storageValue, address, allowedPaths) case sema.Account_StorageTypeSaveFunctionName: - return authAccountStorageSaveFunction(context, storageValue, address) + return authAccountStorageSaveFunction(context, storageValue, address, allowedPaths) case sema.Account_StorageTypeBorrowFunctionName: - return authAccountStorageBorrowFunction(context, storageValue, address) + return authAccountStorageBorrowFunction(context, storageValue, address, allowedPaths) case sema.Account_StorageTypeCheckFunctionName: - return authAccountStorageCheckFunction(context, storageValue, address) + return authAccountStorageCheckFunction(context, storageValue, address, allowedPaths) + + case sema.Account_StorageTypeLimitedToPathsFunctionName: + return accountStorageLimitedToPathsFunction( + context, + storageValue, + address, + storageUsedGet, + storageCapacityGet, + allowedPaths, + ) } return nil @@ -90,10 +106,10 @@ func NewAccountStorageValue( computeField := func(name string, context MemberAccessibleContext) Value { switch name { case sema.Account_StorageTypePublicPathsFieldName: - return publicAccountPaths(context, address) + return publicAccountPaths(context, address, allowedPaths) case sema.Account_StorageTypeStoragePathsFieldName: - return storageAccountPaths(context, address) + return storageAccountPaths(context, address, allowedPaths) case sema.Account_StorageTypeUsedFieldName: return storageUsedGet(context) @@ -142,3 +158,86 @@ func NewAccountStorageValue( return storageValue } + +// isPathAllowed returns true if allowedPaths is nil (unlimited) or the path is in the set. +func isPathAllowed(path PathValue, allowedPaths map[PathValue]struct{}) bool { + if allowedPaths == nil { + return true + } + _, ok := allowedPaths[path] + return ok +} + +// accountStorageLimitedToPathsFunction creates the bound function for limitedToPaths. +func accountStorageLimitedToPathsFunction( + context FunctionCreationContext, + storageValue *SimpleCompositeValue, + address AddressValue, + storageUsedGet func(context MemberAccessibleContext) UInt64Value, + storageCapacityGet func(context MemberAccessibleContext) UInt64Value, + existingAllowedPaths map[PathValue]struct{}, +) BoundFunctionValue { + + return NewBoundHostFunctionValue( + context, + storageValue, + sema.Account_StorageTypeLimitedToPathsFunctionType, + func( + context NativeFunctionContext, + _ TypeArgumentsIterator, + _ ArgumentTypesIterator, + _ Value, + args []Value, + ) Value { + pathsArray, ok := args[0].(*ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + newAllowedPaths := make(map[PathValue]struct{}) + pathsArray.Iterate( + context, + func(element Value) (resume bool) { + pathValue, ok := element.(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + // If there is an existing allowlist (nested limitedToPaths call), + // only include paths that are in both sets (intersection). + if !isPathAllowed(pathValue, existingAllowedPaths) { + return true + } + newAllowedPaths[pathValue] = struct{}{} + return true + }, + false, + ) + + limitedStorageValue := NewAccountStorageValue( + context, + address, + storageUsedGet, + storageCapacityGet, + newAllowedPaths, + ) + + authorization := NewEntitlementSetAuthorization( + context, + func() []common.TypeID { + return []common.TypeID{ + sema.StorageType.ID(), + } + }, + 1, + sema.Conjunction, + ) + + return NewEphemeralReferenceValue( + context, + authorization, + limitedStorageValue, + sema.Account_StorageType, + ) + }, + ) +} diff --git a/sema/account.cdc b/sema/account.cdc index 625684681e..2373b23e65 100644 --- a/sema/account.cdc +++ b/sema/account.cdc @@ -159,6 +159,12 @@ struct Account { /// Otherwise, iteration aborts. access(all) fun forEachStored(_ function: fun (StoragePath, Type): Bool) + + /// Returns a new reference to this account's storage that only allows + /// access to the given paths. Operations on any other path behave as if + /// nothing is stored there. Saving to a non-allowed path aborts. + access(Storage) + fun limitedToPaths(_ paths: [Path]): auth(Storage) &Account.Storage } access(all) diff --git a/sema/account.gen.go b/sema/account.gen.go index 930895948b..4487d16ae0 100644 --- a/sema/account.gen.go +++ b/sema/account.gen.go @@ -439,6 +439,37 @@ then the callback must stop iteration by returning false. Otherwise, iteration aborts. ` +const Account_StorageTypeLimitedToPathsFunctionName = "limitedToPaths" + +var Account_StorageTypeLimitedToPathsFunctionType = &FunctionType{ + Parameters: []Parameter{ + { + Label: ArgumentLabelNotRequired, + Identifier: "paths", + TypeAnnotation: NewTypeAnnotation(&VariableSizedType{ + Type: PathType, + }), + }, + }, + ReturnTypeAnnotation: NewTypeAnnotation( + &ReferenceType{ + Type: Account_StorageType, + // NOTE: manually fixed because the code generator does not yet + // support entitlements on reference return types (see gen/main.go). + Authorization: newEntitlementAccess( + []Type{StorageType}, + Conjunction, + ), + }, + ), +} + +const Account_StorageTypeLimitedToPathsFunctionDocString = ` +Returns a new reference to this account's storage that only allows +access to the given paths. Operations on any other path behave as if +nothing is stored there. Saving to a non-allowed path aborts. +` + const Account_StorageTypeName = "Storage" var Account_StorageType = func() *CompositeType { @@ -554,6 +585,16 @@ func init() { Account_StorageTypeForEachStoredFunctionType, Account_StorageTypeForEachStoredFunctionDocString, ), + NewUnmeteredFunctionMember( + Account_StorageType, + newEntitlementAccess( + []Type{StorageType}, + Conjunction, + ), + Account_StorageTypeLimitedToPathsFunctionName, + Account_StorageTypeLimitedToPathsFunctionType, + Account_StorageTypeLimitedToPathsFunctionDocString, + ), } Account_StorageType.Members = MembersAsMap(members) diff --git a/stdlib/account.go b/stdlib/account.go index 3d4de2eef9..431b9ee0f0 100644 --- a/stdlib/account.go +++ b/stdlib/account.go @@ -418,6 +418,7 @@ func newAccountStorageValue( addressValue, newStorageUsedGetFunction(handler, addressValue), newStorageCapacityGetFunction(handler, addressValue), + nil, // no path restrictions (unlimited storage) ) } From 4f3f6c00d5787a271694c49eb57f1dc5c47a3482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 13 Mar 2026 14:59:10 -0700 Subject: [PATCH 2/3] add support for authorizations to code generator --- sema/account.gen.go | 2 -- sema/gen/main.go | 76 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/sema/account.gen.go b/sema/account.gen.go index 4487d16ae0..70e1806663 100644 --- a/sema/account.gen.go +++ b/sema/account.gen.go @@ -454,8 +454,6 @@ var Account_StorageTypeLimitedToPathsFunctionType = &FunctionType{ ReturnTypeAnnotation: NewTypeAnnotation( &ReferenceType{ Type: Account_StorageType, - // NOTE: manually fixed because the code generator does not yet - // support entitlements on reference return types (see gen/main.go). Authorization: newEntitlementAccess( []Type{StorageType}, Conjunction, diff --git a/sema/gen/main.go b/sema/gen/main.go index ec3cbebc66..9f3b6f6699 100644 --- a/sema/gen/main.go +++ b/sema/gen/main.go @@ -931,14 +931,7 @@ func typeExpr(t ast.Type, typeParams map[string]string) dst.Expr { }, Elts: []dst.Expr{ goKeyValue("Type", borrowType), - // TODO: add support for parsing entitlements - goKeyValue( - "Authorization", - &dst.Ident{ - Name: "UnauthorizedAccess", - Path: semaPath, - }, - ), + goKeyValue("Authorization", authorizationExpr(t.Authorization)), }, }, } @@ -1911,6 +1904,73 @@ func accessExpr(access ast.Access) dst.Expr { } } +func authorizationExpr(authorization ast.Authorization) dst.Expr { + if authorization == nil { + return &dst.Ident{ + Name: "UnauthorizedAccess", + Path: semaPath, + } + } + + switch authorization := authorization.(type) { + case ast.EntitlementSet: + entitlements := authorization.Entitlements() + + entitlementExprs := make([]dst.Expr, 0, len(entitlements)) + + for _, nominalType := range entitlements { + entitlementExpr := typeExpr(nominalType, nil) + entitlementExprs = append(entitlementExprs, entitlementExpr) + } + + var setKind dst.Expr + + switch authorization.Separator() { + case ast.Conjunction: + setKind = &dst.Ident{ + Name: "Conjunction", + Path: semaPath, + } + case ast.Disjunction: + setKind = &dst.Ident{ + Name: "Disjunction", + Path: semaPath, + } + default: + panic(errors.NewUnreachableError()) + } + + args := []dst.Expr{ + &dst.CompositeLit{ + Type: &dst.ArrayType{ + Elt: &dst.Ident{ + Name: "Type", + Path: semaPath, + }, + }, + Elts: entitlementExprs, + }, + setKind, + } + + for _, arg := range args { + arg.Decorations().Before = dst.NewLine + arg.Decorations().After = dst.NewLine + } + + return &dst.CallExpr{ + Fun: &dst.Ident{ + Name: "newEntitlementAccess", + Path: semaPath, + }, + Args: args, + } + + default: + panic(fmt.Errorf("unsupported authorization: %#+v\n", authorization)) + } +} + func variableKindIdent(variableKind ast.VariableKind) *dst.Ident { return &dst.Ident{ Name: variableKind.String(), From 1a57c6931d0a35498d4943a3be846e7390aee59b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastian=20M=C3=BCller?= Date: Fri, 13 Mar 2026 15:50:48 -0700 Subject: [PATCH 3/3] add support for Account.Storage.limitedToPaths to compiler/VM --- bbq/vm/value_account_storage.go | 10 ++ interpreter/interpreter.go | 14 +-- interpreter/native_function.go | 5 +- interpreter/simplecompositevalue.go | 8 +- interpreter/value_account_storage.go | 162 +++++++++++++++++---------- 5 files changed, 130 insertions(+), 69 deletions(-) diff --git a/bbq/vm/value_account_storage.go b/bbq/vm/value_account_storage.go index bd0c5e7e6a..7784e0b4e3 100644 --- a/bbq/vm/value_account_storage.go +++ b/bbq/vm/value_account_storage.go @@ -110,4 +110,14 @@ func init() { interpreter.NativeAccountStorageCheckFunction(nil, nil), ), ) + + // Account.Storage.limitedToPaths + registerBuiltinTypeBoundFunction( + accountStorageTypeName, + NewNativeFunctionValue( + sema.Account_StorageTypeLimitedToPathsFunctionName, + sema.Account_StorageTypeLimitedToPathsFunctionType, + interpreter.NativeAccountStorageLimitedToPathsFunction(nil, nil), + ), + ) } diff --git a/interpreter/interpreter.go b/interpreter/interpreter.go index 65b8463087..964bb91641 100644 --- a/interpreter/interpreter.go +++ b/interpreter/interpreter.go @@ -4716,7 +4716,7 @@ func NativeAccountStorageIterateFunction( address, domain, pathType, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -4942,7 +4942,7 @@ func NativeAccountStorageSaveFunction( context, args, addressValue, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -5036,7 +5036,7 @@ func NativeAccountStorageTypeFunction( context, args, address, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -5140,7 +5140,7 @@ func NativeAccountStorageCopyFunction( args, semaBorrowType, address, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -5164,7 +5164,7 @@ func NativeAccountStorageLoadFunction( args, semaBorrowType, address, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -5327,7 +5327,7 @@ func NativeAccountStorageBorrowFunction( args, typeParameter, address, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } @@ -5410,7 +5410,7 @@ func NativeAccountStorageCheckFunction( address, args, typeArgument, - allowedPaths, + getEffectiveAllowedPaths(receiver, allowedPaths), ) } } diff --git a/interpreter/native_function.go b/interpreter/native_function.go index 367c2cf2fb..9c8f095555 100644 --- a/interpreter/native_function.go +++ b/interpreter/native_function.go @@ -275,6 +275,9 @@ func GetAccountTypePrivateAddressValue(receiver Value) AddressValue { simpleCompositeValue := AssertValueOfType[*SimpleCompositeValue](receiver) addressMetaInfo := simpleCompositeValue.PrivateField(AccountTypePrivateAddressFieldName) - address := AssertValueOfType[AddressValue](addressMetaInfo) + address, ok := addressMetaInfo.(AddressValue) + if !ok { + panic(errors.NewUnreachableError()) + } return address } diff --git a/interpreter/simplecompositevalue.go b/interpreter/simplecompositevalue.go index 2b7a915713..87311c11f1 100644 --- a/interpreter/simplecompositevalue.go +++ b/interpreter/simplecompositevalue.go @@ -48,7 +48,7 @@ type SimpleCompositeValue struct { // privateFields is a property bag to carry internal data // that are not visible to cadence users. // TODO: any better way to pass down information? - privateFields map[string]Value + privateFields map[string]any } var _ Value = &SimpleCompositeValue{} @@ -318,16 +318,16 @@ func (v *SimpleCompositeValue) DeepRemove(_ ValueRemoveContext, _ bool) { // NO-OP } -func (v *SimpleCompositeValue) WithPrivateField(key string, value Value) *SimpleCompositeValue { +func (v *SimpleCompositeValue) WithPrivateField(key string, value any) *SimpleCompositeValue { if v.privateFields == nil { - v.privateFields = make(map[string]Value) + v.privateFields = make(map[string]any) } v.privateFields[key] = value return v } -func (v *SimpleCompositeValue) PrivateField(key string) Value { +func (v *SimpleCompositeValue) PrivateField(key string) any { if v.privateFields == nil { return nil } diff --git a/interpreter/value_account_storage.go b/interpreter/value_account_storage.go index 167065994f..18ee74c3c6 100644 --- a/interpreter/value_account_storage.go +++ b/interpreter/value_account_storage.go @@ -156,6 +156,13 @@ func NewAccountStorageValue( stringer, ).WithPrivateField(AccountTypePrivateAddressFieldName, address) + if allowedPaths != nil { + storageValue = storageValue.WithPrivateField( + accountStoragePrivateAllowedPathsFieldName, + allowedPaths, + ) + } + return storageValue } @@ -168,7 +175,40 @@ func isPathAllowed(path PathValue, allowedPaths map[PathValue]struct{}) bool { return ok } -// accountStorageLimitedToPathsFunction creates the bound function for limitedToPaths. +const accountStoragePrivateAllowedPathsFieldName = "allowedPaths" + +// getAllowedPathsFromReceiver extracts the allowed paths set +// from a receiver's private field. +// Returns nil if no restriction is set. +func getAllowedPathsFromReceiver(receiver Value) map[PathValue]struct{} { + composite, ok := receiver.(*SimpleCompositeValue) + if !ok { + return nil + } + + field := composite.PrivateField(accountStoragePrivateAllowedPathsFieldName) + if field == nil { + return nil + } + + allowedPaths, ok := field.(map[PathValue]struct{}) + if !ok { + return nil + } + + return allowedPaths +} + +// getEffectiveAllowedPaths returns the allowed paths for a storage operation. +// If closureAllowedPaths is non-nil (interpreter path), it is used directly. +// Otherwise, the allowed paths are extracted from the receiver's private field (BBQ VM path). +func getEffectiveAllowedPaths(receiver Value, closureAllowedPaths map[PathValue]struct{}) map[PathValue]struct{} { + if closureAllowedPaths != nil { + return closureAllowedPaths + } + return getAllowedPathsFromReceiver(receiver) +} + func accountStorageLimitedToPathsFunction( context FunctionCreationContext, storageValue *SimpleCompositeValue, @@ -182,62 +222,70 @@ func accountStorageLimitedToPathsFunction( context, storageValue, sema.Account_StorageTypeLimitedToPathsFunctionType, - func( - context NativeFunctionContext, - _ TypeArgumentsIterator, - _ ArgumentTypesIterator, - _ Value, - args []Value, - ) Value { - pathsArray, ok := args[0].(*ArrayValue) - if !ok { - panic(errors.NewUnreachableError()) - } - - newAllowedPaths := make(map[PathValue]struct{}) - pathsArray.Iterate( - context, - func(element Value) (resume bool) { - pathValue, ok := element.(PathValue) - if !ok { - panic(errors.NewUnreachableError()) - } - // If there is an existing allowlist (nested limitedToPaths call), - // only include paths that are in both sets (intersection). - if !isPathAllowed(pathValue, existingAllowedPaths) { - return true - } - newAllowedPaths[pathValue] = struct{}{} - return true - }, - false, - ) - - limitedStorageValue := NewAccountStorageValue( - context, - address, - storageUsedGet, - storageCapacityGet, - newAllowedPaths, - ) + NativeAccountStorageLimitedToPathsFunction(&address, existingAllowedPaths), + ) +} - authorization := NewEntitlementSetAuthorization( - context, - func() []common.TypeID { - return []common.TypeID{ - sema.StorageType.ID(), - } - }, - 1, - sema.Conjunction, - ) +func NativeAccountStorageLimitedToPathsFunction( + addressPointer *AddressValue, + allowedPaths map[PathValue]struct{}, +) NativeFunction { + return func( + context NativeFunctionContext, + _ TypeArgumentsIterator, + _ ArgumentTypesIterator, + receiver Value, + args []Value, + ) Value { + address := GetAddressValue(receiver, addressPointer) + existingAllowedPaths := getEffectiveAllowedPaths(receiver, allowedPaths) + + pathsArray, ok := args[0].(*ArrayValue) + if !ok { + panic(errors.NewUnreachableError()) + } - return NewEphemeralReferenceValue( - context, - authorization, - limitedStorageValue, - sema.Account_StorageType, - ) - }, - ) + newAllowedPaths := make(map[PathValue]struct{}) + pathsArray.Iterate( + context, + func(element Value) (resume bool) { + pathValue, ok := element.(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + if !isPathAllowed(pathValue, existingAllowedPaths) { + return true + } + newAllowedPaths[pathValue] = struct{}{} + return true + }, + false, + ) + + limitedStorageValue := NewAccountStorageValue( + context, + address, + nil, + nil, + newAllowedPaths, + ) + + authorization := NewEntitlementSetAuthorization( + context, + func() []common.TypeID { + return []common.TypeID{ + sema.StorageType.ID(), + } + }, + 1, + sema.Conjunction, + ) + + return NewEphemeralReferenceValue( + context, + authorization, + limitedStorageValue, + sema.Account_StorageType, + ) + } }