diff --git a/engine/execution/state/bootstrap/bootstrap_test.go b/engine/execution/state/bootstrap/bootstrap_test.go index 524e4e62b27..151a69a5c24 100644 --- a/engine/execution/state/bootstrap/bootstrap_test.go +++ b/engine/execution/state/bootstrap/bootstrap_test.go @@ -44,7 +44,7 @@ func TestBootstrapLedger(t *testing.T) { } func TestBootstrapLedger_ZeroTokenSupply(t *testing.T) { - var expectedStateCommitment, _ = hex.DecodeString("d1ac84222a9e6312d288f918b16f1c887128ee1a1fb7506ad4c3e55a0b491f7b") + var expectedStateCommitment, _ = hex.DecodeString("f3b24f02064671fd2384c4283a60afbfecf94306796b4881cd915524886f19a5") unittest.RunWithTempDir(t, func(dbDir string) { diff --git a/engine/execution/state/delta/view.go b/engine/execution/state/delta/view.go index 0fb0534fa00..573be452fe6 100644 --- a/engine/execution/state/delta/view.go +++ b/engine/execution/state/delta/view.go @@ -147,6 +147,10 @@ func (v *View) Set(owner, controller, key string, value flow.RegisterValue) { v.delta.Set(owner, controller, key, value) } +func (v *View) RegisterUpdates() ([]flow.RegisterID, []flow.RegisterValue) { + return v.delta.RegisterUpdates() +} + func (v *View) updateSpock(value []byte) error { _, err := v.spockSecretHasher.Write(value) if err != nil { diff --git a/fvm/context.go b/fvm/context.go index 5ef31bc754e..7cf960d9f5e 100644 --- a/fvm/context.go +++ b/fvm/context.go @@ -66,6 +66,7 @@ func defaultContext() Context { NewTransactionSequenceNumberChecker(), NewTransactionFeeDeductor(), NewTransactionInvocator(), + NewTransactionStorageLimiter(), }, ScriptProcessors: []ScriptProcessor{ NewScriptInvocator(), diff --git a/fvm/errors.go b/fvm/errors.go index d1610391ef0..8d453493c0f 100644 --- a/fvm/errors.go +++ b/fvm/errors.go @@ -25,6 +25,8 @@ const ( errCodeInvalidHashAlgorithm = 10 + errCodeStorageCapacityExceeded = 11 + errCodeExecution = 100 ) @@ -213,6 +215,21 @@ func (e *InvalidHashAlgorithmError) Code() uint32 { return errCodeInvalidHashAlgorithm } +// An StorageCapacityExceededError indicates that a given key has an invalid hash algorithm. +type StorageCapacityExceededError struct { + Address flow.Address + StorageUsed uint64 + StorageCapacity uint64 +} + +func (e *StorageCapacityExceededError) Error() string { + return fmt.Sprintf("address %s storage %d is over capacity %d", e.Address, e.StorageUsed, e.StorageCapacity) +} + +func (e *StorageCapacityExceededError) Code() uint32 { + return errCodeStorageCapacityExceeded +} + type ExecutionError struct { Err runtime.Error } diff --git a/fvm/fvm_test.go b/fvm/fvm_test.go index 4c27ea9a239..2ea0acfb5cb 100644 --- a/fvm/fvm_test.go +++ b/fvm/fvm_test.go @@ -2,6 +2,7 @@ package fvm_test import ( "crypto/rand" + "encoding/base64" "fmt" "strconv" "testing" @@ -16,6 +17,7 @@ import ( "github.com/onflow/flow-go/crypto" "github.com/onflow/flow-go/crypto/hash" + "github.com/onflow/flow-go/engine/execution/state/delta" "github.com/onflow/flow-go/engine/execution/testutil" "github.com/onflow/flow-go/fvm" fvmmock "github.com/onflow/flow-go/fvm/mock" @@ -47,7 +49,8 @@ func vmTest( ctx := fvm.NewContext(opts...) - ledger := state.NewMapLedger() + mapLedger := state.NewMapLedger() + ledger := delta.NewView(mapLedger.Get) err = vm.Run( ctx, @@ -455,6 +458,54 @@ func TestBlockContext_ExecuteTransaction_GasLimit(t *testing.T) { } } +func TestBlockContext_ExecuteTransaction_StorageLimit(t *testing.T) { + t.Run("Storing too much data fails", vmTest( + func(t *testing.T, vm *fvm.VirtualMachine, chain flow.Chain, ctx fvm.Context, ledger state.Ledger) { + // Create an account private key. + privateKeys, err := testutil.GenerateAccountPrivateKeys(1) + require.NoError(t, err) + + // Bootstrap a ledger, creating accounts with the provided private keys and the root account. + accounts, err := testutil.CreateAccounts(vm, ledger, privateKeys, chain) + require.NoError(t, err) + + b := make([]byte, 100000) // 100k bytes + _, err = rand.Read(b) + require.NoError(t, err) + longString := base64.StdEncoding.EncodeToString(b) + txBody := testutil.CreateContractDeploymentTransaction( + "Container", + fmt.Sprintf(` + access(all) contract Container { + access(all) resource Counter { + pub var longString: String + init() { + self.longString = "%s" + } + } + } + `, longString), + accounts[0], + chain) + + txBody.SetProposalKey(chain.ServiceAddress(), 0, 0) + txBody.SetPayer(chain.ServiceAddress()) + + err = testutil.SignPayload(txBody, accounts[0], privateKeys[0]) + require.NoError(t, err) + + err = testutil.SignEnvelope(txBody, chain.ServiceAddress(), unittest.ServiceAccountPrivateKey) + require.NoError(t, err) + + tx := fvm.Transaction(txBody) + + err = vm.Run(ctx, tx, ledger) + require.NoError(t, err) + + assert.Equal(t, (&fvm.StorageCapacityExceededError{}).Code(), tx.Err.Code()) + })) +} + var createAccountScript = []byte(` transaction { prepare(signer: AuthAccount) { diff --git a/fvm/state/accounts.go b/fvm/state/accounts.go index 3adb4e1c04a..fa423af021d 100644 --- a/fvm/state/accounts.go +++ b/fvm/state/accounts.go @@ -14,12 +14,13 @@ import ( ) const ( - keyExists = "exists" - keyCode = "code" - keyContractNames = "contract_names" - keyPublicKeyCount = "public_key_count" - keyStorageUsed = "storage_used" - uint64StorageSize = 8 + keyExists = "exists" + keyCode = "code" + keyContractNames = "contract_names" + keyPublicKeyCount = "public_key_count" + keyStorageUsed = "storage_used" + keyStorageCapacity = "storage_capacity" + uint64StorageSize = 8 ) var ( @@ -109,6 +110,14 @@ func (a *Accounts) Create(publicKeys []flow.AccountPublicKey, newAddress flow.Ad return err } + // set storage capacity to 0. + // It must be set with the storage contract before the end of this transaction. + // TODO: for this PR set storage capacity to 100kB, remove this in the next PR to this feature branch + err = a.SetStorageCapacity(newAddress, 100000) + if err != nil { + return err + } + // mark that this account exists err = a.setValue(newAddress, false, keyExists, []byte{1}) if err != nil { @@ -348,7 +357,23 @@ func (a *Accounts) setStorageUsed(address flow.Address, used uint64) error { return a.setValue(address, false, keyStorageUsed, usedBinary) } -// GetValue returns a value stored in address' storage +func (a *Accounts) GetStorageCapacity(account flow.Address) (uint64, error) { + storageCapacityRegister, err := a.getValue(account, false, keyStorageCapacity) + if err != nil { + return 0, err + } + storageCapacity, _, err := utils.ReadUint64(storageCapacityRegister) + if err != nil { + return 0, err + } + return storageCapacity, nil +} + +func (a *Accounts) SetStorageCapacity(account flow.Address, capacity uint64) error { + capacityBinary := utils.Uint64ToBinary(capacity) + return a.setValue(account, false, keyStorageCapacity, capacityBinary) +} + func (a *Accounts) GetValue(address flow.Address, key string) (flow.RegisterValue, error) { return a.getValue(address, false, key) } diff --git a/fvm/state/accounts_test.go b/fvm/state/accounts_test.go index 7afabc113ea..c2c08fdcd0f 100644 --- a/fvm/state/accounts_test.go +++ b/fvm/state/accounts_test.go @@ -19,7 +19,8 @@ func TestAccounts_Create(t *testing.T) { err := accounts.Create(nil, address) require.NoError(t, err) - require.Equal(t, len(ledger.RegisterTouches), 3) // storage_used + exists + key count + // storage_used + storage_capacity + exists + code + key count + require.Equal(t, len(ledger.RegisterTouches), 4) }) t.Run("Fails if account exists", func(t *testing.T) { @@ -173,7 +174,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9)) // exists: 1 byte, storage_used 8 bytes + require.Equal(t, storageUsed, uint64(17)) // exists: 1 byte, storage_ used & capacity 16 bytes }) t.Run("Storage used on register set increases", func(t *testing.T) { @@ -190,7 +191,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+12)) // exists: 1 byte, storage_used 8 bytes, some_key 12 + require.Equal(t, storageUsed, uint64(17+12)) // exists: 1 byte, storage_ used & capacity 16 bytes, some_key 12 }) t.Run("Storage used, set twice on same register to same value, stays the same", func(t *testing.T) { @@ -209,7 +210,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+12)) // exists: 1 byte, storage_used 8 bytes, some_key 12 + require.Equal(t, storageUsed, uint64(17+12)) // exists: 1 byte, storage_ used & capacity 16 bytes, some_key 12 }) t.Run("Storage used, set twice on same register to larger value, increases", func(t *testing.T) { @@ -228,7 +229,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+13)) // exists: 1 byte, storage_used 8 bytes, some_key 13 + require.Equal(t, storageUsed, uint64(17+13)) // exists: 1 byte, storage_ used & capacity 16 bytes, some_key 13 }) t.Run("Storage used, set twice on same register to smaller value, decreases", func(t *testing.T) { @@ -247,7 +248,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+11)) // exists: 1 byte, storage_used 8 bytes, some_key 11 + require.Equal(t, storageUsed, uint64(17+11)) // exists: 1 byte, storage_ used & capacity 16 bytes, some_key 11 }) t.Run("Storage used, after register deleted, decreases", func(t *testing.T) { @@ -266,7 +267,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+0)) // exists: 1 byte, storage_used 8 bytes, some_key 0 + require.Equal(t, storageUsed, uint64(17+0)) // exists: 1 byte, storage_ used & capacity 16 bytes, some_key 0 }) t.Run("Storage used on a complex scenario has correct value", func(t *testing.T) { @@ -290,7 +291,7 @@ func TestAccount_StorageUsed(t *testing.T) { storageUsed, err := accounts.GetStorageUsed(address) require.NoError(t, err) - require.Equal(t, storageUsed, uint64(9+34)) // exists: 1 byte, storage_used 8 bytes, other 34 + require.Equal(t, storageUsed, uint64(17+34)) // exists: 1 byte, storage_ used & capacity 16 bytes, other 34 }) } diff --git a/fvm/state/ledger.go b/fvm/state/ledger.go index 420b0c5e54e..754c76ba763 100644 --- a/fvm/state/ledger.go +++ b/fvm/state/ledger.go @@ -12,19 +12,20 @@ type Ledger interface { Get(owner, controller, key string) (flow.RegisterValue, error) Touch(owner, controller, key string) Delete(owner, controller, key string) + RegisterUpdates() ([]flow.RegisterID, []flow.RegisterValue) } // A MapLedger is a naive ledger storage implementation backed by a simple map. // // This implementation is designed for testing purposes. type MapLedger struct { - Registers map[string]flow.RegisterValue + Registers map[string]flow.RegisterEntry RegisterTouches map[string]bool } func NewMapLedger() *MapLedger { return &MapLedger{ - Registers: make(map[string]flow.RegisterValue), + Registers: make(map[string]flow.RegisterEntry), RegisterTouches: make(map[string]bool), } } @@ -32,13 +33,20 @@ func NewMapLedger() *MapLedger { func (m MapLedger) Set(owner, controller, key string, value flow.RegisterValue) { k := fullKey(owner, controller, key) m.RegisterTouches[k] = true - m.Registers[k] = value + m.Registers[k] = flow.RegisterEntry{ + Key: flow.RegisterID{ + Owner: owner, + Controller: controller, + Key: key, + }, + Value: value, + } } func (m MapLedger) Get(owner, controller, key string) (flow.RegisterValue, error) { k := fullKey(owner, controller, key) m.RegisterTouches[k] = true - return m.Registers[k], nil + return m.Registers[k].Value, nil } func (m MapLedger) Touch(owner, controller, key string) { @@ -49,6 +57,24 @@ func (m MapLedger) Delete(owner, controller, key string) { delete(m.Registers, fullKey(owner, controller, key)) } +func (m MapLedger) RegisterUpdates() ([]flow.RegisterID, []flow.RegisterValue) { + data := make(flow.RegisterEntries, 0, len(m.Registers)) + + for _, v := range m.Registers { + data = append(data, v) + } + + ids := make([]flow.RegisterID, 0, len(m.Registers)) + values := make([]flow.RegisterValue, 0, len(m.Registers)) + + for _, v := range data { + ids = append(ids, v.Key) + values = append(values, v.Value) + } + + return ids, values +} + func fullKey(owner, controller, key string) string { // https://en.wikipedia.org/wiki/C0_and_C1_control_codes#Field_separators return strings.Join([]string{owner, controller, key}, "\x1F") diff --git a/fvm/transactionStorageLimiter.go b/fvm/transactionStorageLimiter.go new file mode 100644 index 00000000000..2e1aec820a5 --- /dev/null +++ b/fvm/transactionStorageLimiter.go @@ -0,0 +1,77 @@ +package fvm + +import ( + "github.com/onflow/flow-go/fvm/state" + "github.com/onflow/flow-go/model/flow" +) + +type TransactionStorageLimiter struct{} + +func NewTransactionStorageLimiter() *TransactionStorageLimiter { + return &TransactionStorageLimiter{} +} + +func (d *TransactionStorageLimiter) Process( + _ *VirtualMachine, + _ Context, + _ *TransactionProcedure, + ledger state.Ledger, +) error { + accounts := state.NewAccounts(ledger) + + registerIds, _ := ledger.RegisterUpdates() + + checked := map[string]struct{}{} + + for _, id := range registerIds { + owner := id.Owner + + if _, wasChecked := checked[owner]; wasChecked { + // we already checked this account, move on + continue + } + checked[owner] = struct{}{} + + // is this an address? + address, isAddress := addressFromString(owner) + if !isAddress { + continue + } + // does it exist? + exists, err := accounts.Exists(address) + if err != nil { + return err + } + if !exists { + continue + } + + capacity, err := accounts.GetStorageCapacity(address) + if err != nil { + return err + } + usage, err := accounts.GetStorageUsed(address) + if err != nil { + return err + } + + if usage > capacity { + return &StorageCapacityExceededError{ + Address: flow.HexToAddress(owner), + StorageUsed: usage, + StorageCapacity: capacity, + } + } + } + return nil +} + +func addressFromString(owner string) (flow.Address, bool) { + ownerBytes := []byte(owner) + if len(ownerBytes) != flow.AddressLength { + // not an address + return flow.EmptyAddress, false + } + address := flow.BytesToAddress(ownerBytes) + return address, true +} diff --git a/fvm/transactionStorageLimiter_test.go b/fvm/transactionStorageLimiter_test.go new file mode 100644 index 00000000000..e5a20594158 --- /dev/null +++ b/fvm/transactionStorageLimiter_test.go @@ -0,0 +1,201 @@ +package fvm_test + +import ( + "encoding/binary" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/fvm" + "github.com/onflow/flow-go/model/flow" +) + +func TestTransactionStorageLimiter_Process(t *testing.T) { + t.Run("capacity > storage -> OK", func(t *testing.T) { + owner := string(flow.HexToAddress("1").Bytes()) + ledger := newMockLedger( + []string{owner}, + []OwnerKeyValue{ + storageUsed(owner, 99), + storageCapacity(owner, 100), + accountExists(owner), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err, "Transaction with higher capacity than storage used should work") + }) + t.Run("capacity = storage -> OK", func(t *testing.T) { + owner := string(flow.HexToAddress("1").Bytes()) + ledger := newMockLedger( + []string{owner}, + []OwnerKeyValue{ + storageUsed(owner, 100), + storageCapacity(owner, 100), + accountExists(owner), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err, "Transaction with equal capacity than storage used should work") + }) + t.Run("capacity < storage -> Not OK", func(t *testing.T) { + owner := string(flow.HexToAddress("1").Bytes()) + ledger := newMockLedger( + []string{owner}, + []OwnerKeyValue{ + storageUsed(owner, 101), + storageCapacity(owner, 100), + accountExists(owner), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.Error(t, err, "Transaction with lower capacity than storage used should fail") + }) + t.Run("if two registers change on the same account, only check capacity once", func(t *testing.T) { + owner := string(flow.HexToAddress("1").Bytes()) + ledger := newMockLedger( + []string{owner, owner}, + []OwnerKeyValue{ + storageUsed(owner, 99), + storageCapacity(owner, 100), + accountExists(owner), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err) + // three touches per account: get exists, get capacity, get used + require.Equal(t, 3, ledger.GetCalls[owner]) + }) + t.Run("two registers change on different accounts, only check capacity once per account", func(t *testing.T) { + owner1 := string(flow.HexToAddress("1").Bytes()) + owner2 := string(flow.HexToAddress("2").Bytes()) + ledger := newMockLedger( + []string{owner1, owner1, owner2, owner2}, + []OwnerKeyValue{ + storageUsed(owner1, 99), + storageCapacity(owner1, 100), + accountExists(owner2), + storageUsed(owner2, 999), + storageCapacity(owner2, 1000), + accountExists(owner2), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err) + // three touches per account: get exists, get capacity, get used + require.Equal(t, 3, ledger.GetCalls[owner1]) + require.Equal(t, 3, ledger.GetCalls[owner2]) + }) + t.Run("non account registers are ignored", func(t *testing.T) { + owner := "" + ledger := newMockLedger( + []string{owner}, + []OwnerKeyValue{ + storageUsed(owner, 101), + storageCapacity(owner, 100), + accountExists(owner), // it has exists value, but it cannot be parsed as an address + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err) + }) + t.Run("account registers without exists are ignored", func(t *testing.T) { + owner := string(flow.HexToAddress("1").Bytes()) + ledger := newMockLedger( + []string{owner}, + []OwnerKeyValue{ + storageUsed(owner, 101), + storageCapacity(owner, 100), + }) + d := &fvm.TransactionStorageLimiter{} + + err := d.Process(nil, fvm.Context{}, nil, ledger) + + require.NoError(t, err) + }) +} + +type MockLedger struct { + UpdatedRegisterKeys []flow.RegisterID + StorageValues map[string]map[string]flow.RegisterValue + GetCalls map[string]int +} + +type OwnerKeyValue struct { + Owner string + Key string + Value uint64 +} + +func storageUsed(owner string, value uint64) OwnerKeyValue { + return OwnerKeyValue{ + Owner: owner, + Key: "storage_used", + Value: value, + } +} + +func storageCapacity(owner string, value uint64) OwnerKeyValue { + return OwnerKeyValue{ + Owner: owner, + Key: "storage_capacity", + Value: value, + } +} + +func accountExists(owner string) OwnerKeyValue { + return OwnerKeyValue{ + Owner: owner, + Key: "exists", + Value: 1, + } +} + +func newMockLedger(updatedKeys []string, ownerKeyStorageValue []OwnerKeyValue) MockLedger { + storageValues := make(map[string]map[string]flow.RegisterValue) + for _, okv := range ownerKeyStorageValue { + _, exists := storageValues[okv.Owner] + if !exists { + storageValues[okv.Owner] = make(map[string]flow.RegisterValue) + } + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, okv.Value) + storageValues[okv.Owner][okv.Key] = buf + } + updatedRegisters := make([]flow.RegisterID, len(updatedKeys)) + for i, key := range updatedKeys { + updatedRegisters[i] = flow.RegisterID{ + Owner: key, + Controller: "", + Key: "", + } + } + + return MockLedger{ + UpdatedRegisterKeys: updatedRegisters, + StorageValues: storageValues, + GetCalls: make(map[string]int), + } +} + +func (l MockLedger) Set(_, _, _ string, _ flow.RegisterValue) {} +func (l MockLedger) Get(owner, _, key string) (flow.RegisterValue, error) { + l.GetCalls[owner] = l.GetCalls[owner] + 1 + return l.StorageValues[owner][key], nil +} +func (l MockLedger) Touch(_, _, _ string) {} +func (l MockLedger) Delete(_, _, _ string) {} +func (l MockLedger) RegisterUpdates() ([]flow.RegisterID, []flow.RegisterValue) { + return l.UpdatedRegisterKeys, []flow.RegisterValue{} +} diff --git a/utils/unittest/execution_state.go b/utils/unittest/execution_state.go index a77aaafc301..218f042da66 100644 --- a/utils/unittest/execution_state.go +++ b/utils/unittest/execution_state.go @@ -19,7 +19,7 @@ import ( const ServiceAccountPrivateKeyHex = "e3a08ae3d0461cfed6d6f49bfc25fa899351c39d1bd21fdba8c87595b6c49bb4cc430201" // Pre-calculated state commitment with root account with the above private key -const GenesisStateCommitmentHex = "45c68eb857d76f6e9281e489911b135ead337ff477008b1e596bedb90fffb175" +const GenesisStateCommitmentHex = "7291027f2d25a217217a48fd68d2eadc2284f0c668780798c165e60a211c6ddd" var GenesisStateCommitment flow.StateCommitment