diff options
Diffstat (limited to 'ledger/internal/appcow_test.go')
-rw-r--r-- | ledger/internal/appcow_test.go | 1333 |
1 files changed, 1333 insertions, 0 deletions
diff --git a/ledger/internal/appcow_test.go b/ledger/internal/appcow_test.go new file mode 100644 index 000000000..978854eb8 --- /dev/null +++ b/ledger/internal/appcow_test.go @@ -0,0 +1,1333 @@ +// Copyright (C) 2019-2021 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package internal + +import ( + "fmt" + "math/rand" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/ledger/ledgercore" + ledgertesting "github.com/algorand/go-algorand/ledger/testing" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" +) + +type addrApp struct { + addr basics.Address + aidx basics.AppIndex + global bool +} + +type emptyLedger struct { +} + +func (ml *emptyLedger) lookup(addr basics.Address) (basics.AccountData, error) { + return basics.AccountData{}, nil +} + +func (ml *emptyLedger) checkDup(firstValid, lastValid basics.Round, txn transactions.Txid, txl ledgercore.Txlease) error { + return nil +} + +func (ml *emptyLedger) getAssetCreator(assetIdx basics.AssetIndex) (basics.Address, bool, error) { + return basics.Address{}, false, nil +} + +func (ml *emptyLedger) getAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { + return basics.Address{}, false, nil +} + +func (ml *emptyLedger) getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + return basics.Address{}, false, nil +} + +func (ml *emptyLedger) getStorageCounts(addr basics.Address, aidx basics.AppIndex, global bool) (basics.StateSchema, error) { + return basics.StateSchema{}, nil +} + +func (ml *emptyLedger) getStorageLimits(addr basics.Address, aidx basics.AppIndex, global bool) (basics.StateSchema, error) { + return basics.StateSchema{}, nil +} + +func (ml *emptyLedger) allocated(addr basics.Address, aidx basics.AppIndex, global bool) (bool, error) { + return false, nil +} + +func (ml *emptyLedger) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { + return basics.TealValue{}, false, nil +} + +func (ml *emptyLedger) txnCounter() uint64 { + return 0 +} + +func (ml *emptyLedger) blockHdr(rnd basics.Round) (bookkeeping.BlockHeader, error) { + return bookkeeping.BlockHeader{}, nil +} + +func (ml *emptyLedger) compactCertNext() basics.Round { + return basics.Round(0) +} + +type modsData struct { + addr basics.Address + cidx basics.CreatableIndex + ctype basics.CreatableType +} + +func getCow(creatables []modsData) *roundCowState { + cs := &roundCowState{ + mods: ledgercore.MakeStateDelta(&bookkeeping.BlockHeader{}, 0, 2, 0), + proto: config.Consensus[protocol.ConsensusCurrentVersion], + } + for _, e := range creatables { + cs.mods.Creatables[e.cidx] = ledgercore.ModifiedCreatable{Ctype: e.ctype, Creator: e.addr, Created: true} + } + return cs +} + +// stateTracker tracks the expected state of an account's storage after a +// series of allocs, dallocs, reads, writes, and deletes +type stateTracker struct { + // Expected keys/values for addrApp + storage map[addrApp]basics.TealKeyValue + + // Expected allocation state for addrApp + allocState map[addrApp]bool + + // max StateSchema for addrApp + schemas map[addrApp]basics.StateSchema +} + +func makeStateTracker() stateTracker { + return stateTracker{ + storage: make(map[addrApp]basics.TealKeyValue), + allocState: make(map[addrApp]bool), + schemas: make(map[addrApp]basics.StateSchema), + } +} + +func (st *stateTracker) alloc(aapp addrApp, schema basics.StateSchema) error { + if st.allocated(aapp) { + return fmt.Errorf("already allocated") + } + st.allocState[aapp] = true + st.schemas[aapp] = schema + st.storage[aapp] = make(basics.TealKeyValue) + return nil +} + +func (st *stateTracker) dealloc(aapp addrApp) error { + if !st.allocated(aapp) { + return fmt.Errorf("not allocated") + } + delete(st.allocState, aapp) + delete(st.schemas, aapp) + delete(st.storage, aapp) + return nil +} + +func (st *stateTracker) allocated(aapp addrApp) bool { + return st.allocState[aapp] +} + +func (st *stateTracker) get(aapp addrApp, key string) (basics.TealValue, bool, error) { + if !st.allocated(aapp) { + return basics.TealValue{}, false, fmt.Errorf("not allocated") + } + val, ok := st.storage[aapp][key] + return val, ok, nil +} + +func (st *stateTracker) set(aapp addrApp, key string, val basics.TealValue) error { + if !st.allocated(aapp) { + return fmt.Errorf("not allocated") + } + st.storage[aapp][key] = val + return nil +} + +func (st *stateTracker) del(aapp addrApp, key string) error { + if !st.allocated(aapp) { + return fmt.Errorf("not allocated") + } + delete(st.storage[aapp], key) + return nil +} + +func randomAddrApps(n int) ([]storagePtr, []basics.Address) { + out := make([]storagePtr, n) + outa := make([]basics.Address, n) + for i := 0; i < n; i++ { + out[i] = storagePtr{ + aidx: basics.AppIndex(rand.Intn(100000) + 1), + global: rand.Intn(2) == 0, + } + outa[i] = ledgertesting.RandomAddress() + } + return out, outa +} + +func TestCowStorage(t *testing.T) { + partitiontest.PartitionTest(t) + + ml := emptyLedger{} + var bh bookkeeping.BlockHeader + bh.CurrentProtocol = protocol.ConsensusCurrentVersion + proto, ok := config.Consensus[bh.CurrentProtocol] + require.True(t, ok) + cow := makeRoundCowState(&ml, bh, proto, 0, ledgercore.AccountTotals{}, 0) + allSptrs, allAddrs := randomAddrApps(10) + + st := makeStateTracker() + + var lastParent *roundCowState + const maxChildDepth = 10 + childDepth := 0 + + allKeys := make([]string, 10) + for i := 0; i < len(allKeys); i++ { + allKeys[i] = fmt.Sprintf("%d", i) + } + + allValues := make([]basics.TealValue, 100) + for i := 0; i < len(allValues); i++ { + if i%2 == 0 { + allValues[i] = basics.TealValue{ + Type: basics.TealBytesType, + Bytes: fmt.Sprintf("%d", i), + } + } else { + allValues[i] = basics.TealValue{ + Type: basics.TealUintType, + Uint: uint64(i), + } + } + } + + iters := 1000 + for i := 0; i < iters; i++ { + // Pick a random sptr + r := rand.Intn(len(allSptrs)) + sptr := allSptrs[r] + addr := allAddrs[r] + aapp := addrApp{addr: addr, aidx: sptr.aidx, global: sptr.global} + + // Do some random, valid actions and check that the behavior is + // what we expect + + // Allocate + if rand.Float32() < 0.25 { + actuallyAllocated := st.allocated(aapp) + rschema := basics.StateSchema{ + NumUint: rand.Uint64(), + NumByteSlice: rand.Uint64(), + } + err := cow.AllocateApp(addr, sptr.aidx, sptr.global, rschema) + if actuallyAllocated { + require.Error(t, err) + require.Contains(t, err.Error(), "cannot allocate") + } else { + require.NoError(t, err) + err = st.alloc(aapp, rschema) + require.NoError(t, err) + } + } + + // Deallocate + if rand.Float32() < 0.25 { + actuallyAllocated := st.allocated(aapp) + err := cow.DeallocateApp(addr, sptr.aidx, sptr.global) + if actuallyAllocated { + require.NoError(t, err) + err := st.dealloc(aapp) + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "cannot deallocate") + } + } + + // Write a random key/value + if rand.Float32() < 0.25 { + actuallyAllocated := st.allocated(aapp) + rkey := allKeys[rand.Intn(len(allKeys))] + rval := allValues[rand.Intn(len(allValues))] + err := cow.SetKey(addr, sptr.aidx, sptr.global, rkey, rval, 0) + if actuallyAllocated { + require.NoError(t, err) + err = st.set(aapp, rkey, rval) + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "cannot set") + } + } + + // Delete a random key/value + if rand.Float32() < 0.25 { + actuallyAllocated := st.allocated(aapp) + rkey := allKeys[rand.Intn(len(allKeys))] + err := cow.DelKey(addr, sptr.aidx, sptr.global, rkey, 0) + if actuallyAllocated { + require.NoError(t, err) + err = st.del(aapp, rkey) + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), "cannot del") + } + } + + // Collapse a child + if childDepth > 0 && rand.Float32() < 0.1 { + cow.commitToParent() + cow = lastParent + childDepth-- + } + + // Make a child + if childDepth < maxChildDepth && rand.Float32() < 0.1 { + lastParent = cow + cow = cow.child(1) + childDepth++ + } + + // Check that cow matches our computed state + for i := range allSptrs { + sptr = allSptrs[i] + addr = allAddrs[i] + aapp = addrApp{addr: addr, aidx: sptr.aidx, global: sptr.global} + // Allocations should match + actuallyAllocated := st.allocated(aapp) + cowAllocated, err := cow.allocated(addr, sptr.aidx, sptr.global) + require.NoError(t, err) + require.Equal(t, actuallyAllocated, cowAllocated, fmt.Sprintf("%d, %v, %s", sptr.aidx, sptr.global, addr.String())) + + // All storage should match + if actuallyAllocated { + for _, key := range allKeys { + tval, tok, err := st.get(aapp, key) + require.NoError(t, err) + + cval, cok, err := cow.GetKey(addr, sptr.aidx, sptr.global, key, 0) + require.NoError(t, err) + require.Equal(t, tok, cok) + require.Equal(t, tval, cval) + } + + var numByteSlices uint64 + var numUints uint64 + for _, v := range st.storage[aapp] { + if v.Type == basics.TealBytesType { + numByteSlices++ + } else { + numUints++ + } + } + tcounts := basics.StateSchema{ + NumByteSlice: numByteSlices, + NumUint: numUints, + } + ccounts, err := cow.getStorageCounts(addr, sptr.aidx, sptr.global) + require.NoError(t, err) + require.Equal(t, tcounts, ccounts) + } + } + } +} + +func TestCowBuildDelta(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + creator := ledgertesting.RandomAddress() + sender := ledgertesting.RandomAddress() + aidx := basics.AppIndex(2) + + cow := roundCowState{} + cow.sdeltas = make(map[basics.Address]map[storagePtr]*storageDelta) + txn := transactions.Transaction{} + ed, err := cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Empty(ed) + + cow.sdeltas[creator] = make(map[storagePtr]*storageDelta) + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Empty(ed) + + // check global delta + cow.sdeltas[creator][storagePtr{aidx, true}] = &storageDelta{} + ed, err = cow.BuildEvalDelta(1, &txn) + a.Error(err) + a.Contains(err.Error(), "found storage delta for different app") + a.Empty(ed) + + cow.sdeltas[creator][storagePtr{aidx, true}] = &storageDelta{} + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal(transactions.EvalDelta{GlobalDelta: basics.StateDelta{}}, ed) + + cow.sdeltas[creator][storagePtr{aidx + 1, true}] = &storageDelta{} + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.Error(err) + a.Contains(err.Error(), "found storage delta for different app") + a.Empty(ed) + + delete(cow.sdeltas[creator], storagePtr{aidx + 1, true}) + cow.sdeltas[sender] = make(map[storagePtr]*storageDelta) + cow.sdeltas[sender][storagePtr{aidx, true}] = &storageDelta{} + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.Error(err) + a.Contains(err.Error(), "found more than one global delta") + a.Empty(ed) + + // check local delta + delete(cow.sdeltas[sender], storagePtr{aidx, true}) + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{} + + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.Error(err) + a.Contains(err.Error(), "invalid Account reference ") + a.Empty(ed) + + // check v26 behavior for empty deltas + txn.Sender = sender + cow.mods.Hdr = &bookkeeping.BlockHeader{ + UpgradeState: bookkeeping.UpgradeState{CurrentProtocol: protocol.ConsensusV25}, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta{}, + LocalDeltas: map[uint64]basics.StateDelta{0: {}}, + }, + ed, + ) + + // check v27 behavior for empty deltas + cow.mods.Hdr = nil + cow.proto = config.Consensus[protocol.ConsensusCurrentVersion] + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta{}, + LocalDeltas: map[uint64]basics.StateDelta{}, + }, + ed, + ) + + // check actual serialization + delete(cow.sdeltas[creator], storagePtr{aidx, true}) + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + "key1": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + }, + ed, + ) + + // check empty sender delta (same key update) and non-empty others + delete(cow.sdeltas[sender], storagePtr{aidx, false}) + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + oldExists: true, + newExists: true, + }, + }, + } + txn.Accounts = append(txn.Accounts, creator) + cow.sdeltas[creator][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key2": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + } + + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 1: { + "key2": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + }, + ed, + ) + + // check two keys: empty change and value update + delete(cow.sdeltas[sender], storagePtr{aidx, false}) + delete(cow.sdeltas[creator], storagePtr{aidx, false}) + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + oldExists: true, + newExists: true, + }, + "key2": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + "key2": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + }, + ed, + ) + + // check pre v26 behavior for account index ordering + txn.Sender = sender + txn.Accounts = append(txn.Accounts, sender) + cow.compatibilityMode = true + cow.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + accountIdx: 1, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 1: { + "key1": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + }, + ed, + ) + + // check v27 behavior for account ordering + cow.compatibilityMode = false + cow.sdeltas[sender][storagePtr{aidx, false}] = &storageDelta{ + action: remainAllocAction, + kvCow: stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + }, + accountIdx: 1, + } + ed, err = cow.BuildEvalDelta(aidx, &txn) + a.NoError(err) + a.Equal( + transactions.EvalDelta{ + GlobalDelta: basics.StateDelta(nil), + LocalDeltas: map[uint64]basics.StateDelta{ + 0: { + "key1": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + }, + }, + ed, + ) +} + +func TestCowDeltaSerialize(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + d := stateDelta{ + "key1": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 2}, + oldExists: true, + newExists: true, + }, + } + sd := d.serialize() + a.Equal( + basics.StateDelta{ + "key1": basics.ValueDelta{Action: basics.SetUintAction, Uint: 2}, + }, + sd, + ) + + d = stateDelta{ + "key2": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealBytesType, Bytes: "test"}, + oldExists: true, + newExists: false, + }, + } + sd = d.serialize() + a.Equal( + basics.StateDelta{ + "key2": basics.ValueDelta{Action: basics.DeleteAction}, + }, + sd, + ) + + d = stateDelta{ + "key3": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealBytesType, Bytes: "test"}, + oldExists: false, + newExists: true, + }, + } + sd = d.serialize() + a.Equal( + basics.StateDelta{ + "key3": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "test"}, + }, + sd, + ) + + d = stateDelta{ + "key4": valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 1}, + oldExists: true, + newExists: true, + }, + } + sd = d.serialize() + a.Equal( + basics.StateDelta{}, + sd, + ) +} + +func TestApplyChild(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + emptyStorageDelta := func(action storageAction) storageDelta { + return storageDelta{ + action: action, + kvCow: make(stateDelta), + counts: &basics.StateSchema{}, + maxCounts: &basics.StateSchema{}, + } + } + getSchema := func(u, b int) basics.StateSchema { + return basics.StateSchema{NumUint: uint64(u), NumByteSlice: uint64(b)} + } + + parent := emptyStorageDelta(0) + child := emptyStorageDelta(0) + + chkEmpty := func(delta *storageDelta) { + a.Empty(delta.action) + a.Empty(*delta.counts) + a.Empty(*delta.maxCounts) + a.Equal(0, len(delta.kvCow)) + } + + parent.applyChild(&child) + chkEmpty(&parent) + chkEmpty(&child) + + child.action = deallocAction + child.kvCow["key1"] = valueDelta{} + a.Panics(func() { parent.applyChild(&child) }) + + // check child overwrites values + child.action = allocAction + s1 := getSchema(1, 2) + s2 := getSchema(3, 4) + child.counts = &s1 + child.maxCounts = &s2 + parent.applyChild(&child) + a.Equal(allocAction, parent.action) + a.Equal(1, len(parent.kvCow)) + a.Equal(getSchema(1, 2), *parent.counts) + a.Equal(getSchema(3, 4), *parent.maxCounts) + + // check child is correctly merged into parent + empty := func() valueDelta { + return valueDelta{ + old: basics.TealValue{}, + new: basics.TealValue{}, + oldExists: false, newExists: false, + } + } + created := func(v uint64) valueDelta { + return valueDelta{ + old: basics.TealValue{}, + new: basics.TealValue{Type: basics.TealUintType, Uint: v}, + oldExists: false, newExists: true, + } + } + updated := func(v1, v2 uint64) valueDelta { + return valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: v1}, + new: basics.TealValue{Type: basics.TealUintType, Uint: v2}, + oldExists: true, newExists: true, + } + } + deleted := func(v uint64) valueDelta { + return valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: v}, + new: basics.TealValue{}, + oldExists: true, newExists: false, + } + } + + var tests = []struct { + name string + pkv stateDelta + ckv stateDelta + result stateDelta + }{ + { + // parent and child have unique keys + name: "unique-keys", + pkv: map[string]valueDelta{"key1": created(1), "key2": updated(1, 2), "key3": deleted(3)}, + ckv: map[string]valueDelta{"key4": created(4), "key5": updated(4, 5), "key6": deleted(6)}, + result: map[string]valueDelta{"key1": created(1), "key2": updated(1, 2), "key3": deleted(3), "key4": created(4), "key5": updated(4, 5), "key6": deleted(6)}, + }, + { + // child updates all parent keys + name: "update-keys", + pkv: map[string]valueDelta{"key1": created(1), "key2": updated(1, 2), "key3": deleted(3)}, + ckv: map[string]valueDelta{"key1": updated(1, 2), "key2": updated(2, 3), "key3": updated(0, 4)}, + result: map[string]valueDelta{"key1": created(2), "key2": updated(1, 3), "key3": updated(3, 4)}, + }, + { + // child deletes all parent keys + name: "delete-keys", + pkv: map[string]valueDelta{"key1": created(1), "key2": updated(1, 2), "key3": deleted(3)}, + ckv: map[string]valueDelta{"key1": deleted(1), "key2": deleted(2), "key3": deleted(4)}, + result: map[string]valueDelta{"key1": empty(), "key2": deleted(1), "key3": deleted(3)}, + }, + { + // child re-creates all parent keys + name: "delete-keys", + pkv: map[string]valueDelta{"key1": created(1), "key2": updated(1, 2), "key3": deleted(3)}, + ckv: map[string]valueDelta{"key1": created(2), "key2": created(3), "key3": created(4)}, + result: map[string]valueDelta{"key1": created(2), "key2": updated(1, 3), "key3": updated(3, 4)}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + parent := emptyStorageDelta(0) + ps := getSchema(len(test.pkv), 0) + parent.counts = &ps + parent.kvCow = test.pkv + + child := emptyStorageDelta(remainAllocAction) + cs := getSchema(len(test.ckv)+len(test.pkv), 0) + child.counts = &cs + child.kvCow = test.ckv + + parent.applyChild(&child) + a.Equal(test.result, parent.kvCow) + a.Equal(cs, *parent.counts) + }) + } +} + +func TestApplyStorageDelta(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + created := valueDelta{ + old: basics.TealValue{}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 11}, + oldExists: false, newExists: true, + } + updated := valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 22}, + new: basics.TealValue{Type: basics.TealUintType, Uint: 33}, + oldExists: true, newExists: true, + } + deleted := valueDelta{ + old: basics.TealValue{Type: basics.TealUintType, Uint: 44}, + new: basics.TealValue{}, + oldExists: true, newExists: false, + } + + freshAD := func(kv basics.TealKeyValue) basics.AccountData { + ad := basics.AccountData{} + ad.AppParams = map[basics.AppIndex]basics.AppParams{ + 1: {GlobalState: make(basics.TealKeyValue)}, + 2: {GlobalState: kv}, + } + ad.AppLocalStates = map[basics.AppIndex]basics.AppLocalState{ + 1: {KeyValue: make(basics.TealKeyValue)}, + 2: {KeyValue: kv}, + } + return ad + } + + applyAll := func(kv basics.TealKeyValue, sd *storageDelta) basics.AccountData { + data, err := applyStorageDelta(freshAD(kv), storagePtr{1, true}, sd) + a.NoError(err) + data, err = applyStorageDelta(data, storagePtr{2, true}, sd) + a.NoError(err) + data, err = applyStorageDelta(data, storagePtr{1, false}, sd) + a.NoError(err) + data, err = applyStorageDelta(data, storagePtr{2, false}, sd) + a.NoError(err) + return data + } + + kv := basics.TealKeyValue{ + "key1": basics.TealValue{Type: basics.TealUintType, Uint: 1}, + "key2": basics.TealValue{Type: basics.TealUintType, Uint: 2}, + "key3": basics.TealValue{Type: basics.TealUintType, Uint: 3}, + } + sdu := storageDelta{kvCow: map[string]valueDelta{"key4": created, "key5": updated, "key6": deleted}} + sdd := storageDelta{kvCow: map[string]valueDelta{"key1": created, "key2": updated, "key3": deleted}} + + // check no action + // no op + data := applyAll(kv, &sdu) + a.Equal(0, len(data.AppParams[1].GlobalState)) + a.Equal(len(kv), len(data.AppParams[2].GlobalState)) + a.Equal(0, len(data.AppLocalStates[1].KeyValue)) + a.Equal(len(kv), len(data.AppLocalStates[2].KeyValue)) + + // check dealloc action + // delete all + sdu.action = deallocAction + data = applyAll(kv, &sdu) + a.Equal(0, len(data.AppParams[1].GlobalState)) + a.Equal(0, len(data.AppParams[2].GlobalState)) + a.Equal(0, len(data.AppLocalStates[1].KeyValue)) + a.Equal(0, len(data.AppLocalStates[2].KeyValue)) + + // check alloc action + // re-alloc storage and apply delta + sdu.action = allocAction + data = applyAll(kv, &sdu) + a.Equal(2, len(data.AppParams[1].GlobalState)) + a.Equal(2, len(data.AppParams[2].GlobalState)) + a.Equal(2, len(data.AppLocalStates[1].KeyValue)) + a.Equal(2, len(data.AppLocalStates[2].KeyValue)) + + // check remain action + // unique keys: merge storage and deltas + testUniqueKeys := func(state1 basics.TealKeyValue, state2 basics.TealKeyValue) { + a.Equal(2, len(state1)) + a.Equal(created.new.Uint, state1["key4"].Uint) + a.Equal(updated.new.Uint, state1["key5"].Uint) + + a.Equal(5, len(state2)) + a.Equal(uint64(1), state2["key1"].Uint) + a.Equal(uint64(2), state2["key2"].Uint) + a.Equal(uint64(3), state2["key3"].Uint) + a.Equal(created.new.Uint, state2["key4"].Uint) + a.Equal(updated.new.Uint, state2["key5"].Uint) + } + + sdu.action = remainAllocAction + data = applyAll(kv, &sdu) + testUniqueKeys(data.AppParams[1].GlobalState, data.AppParams[2].GlobalState) + testUniqueKeys(data.AppLocalStates[1].KeyValue, data.AppLocalStates[2].KeyValue) + + // check remain action + // duplicate keys: merge storage and deltas + testDuplicateKeys := func(state1 basics.TealKeyValue, state2 basics.TealKeyValue) { + a.Equal(2, len(state1)) + a.Equal(created.new.Uint, state1["key1"].Uint) + a.Equal(updated.new.Uint, state1["key2"].Uint) + + a.Equal(2, len(state2)) + a.Equal(created.new.Uint, state1["key1"].Uint) + a.Equal(updated.new.Uint, state1["key2"].Uint) + } + + sdd.action = remainAllocAction + data = applyAll(kv, &sdd) + testDuplicateKeys(data.AppParams[1].GlobalState, data.AppParams[2].GlobalState) + testDuplicateKeys(data.AppLocalStates[1].KeyValue, data.AppLocalStates[2].KeyValue) + + sd := storageDelta{action: deallocAction, kvCow: map[string]valueDelta{}} + data, err := applyStorageDelta(basics.AccountData{}, storagePtr{1, true}, &sd) + a.NoError(err) + a.Nil(data.AppParams) + a.Nil(data.AppLocalStates) + a.True(data.IsZero()) + data, err = applyStorageDelta(basics.AccountData{}, storagePtr{1, false}, &sd) + a.NoError(err) + a.Nil(data.AppParams) + a.Nil(data.AppLocalStates) + a.True(data.IsZero()) +} + +func TestCowAllocated(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + aidx := basics.AppIndex(1) + c := getCow([]modsData{}) + + addr1 := ledgertesting.RandomAddress() + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr1: {storagePtr{aidx, false}: &storageDelta{action: allocAction}}, + } + + a.True(c.allocated(addr1, aidx, false)) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.allocated(addr1, aidx+1, false) }) + a.Panics(func() { c.allocated(ledgertesting.RandomAddress(), aidx, false) }) + + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr1: {storagePtr{aidx, true}: &storageDelta{action: allocAction}}, + } + a.True(c.allocated(addr1, aidx, true)) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.allocated(addr1, aidx+1, true) }) + a.Panics(func() { c.allocated(ledgertesting.RandomAddress(), aidx, true) }) +} + +func TestCowGetCreator(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{{addr, basics.CreatableIndex(aidx), basics.AppCreatable}}) + + creator, found, err := c.GetCreator(basics.CreatableIndex(aidx), basics.AssetCreatable) + a.NoError(err) + a.False(found) + a.Equal(creator, basics.Address{}) + + creator, found, err = c.GetCreator(basics.CreatableIndex(aidx), basics.AppCreatable) + a.NoError(err) + a.True(found) + a.Equal(addr, creator) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.GetCreator(basics.CreatableIndex(aidx+1), basics.AppCreatable) }) +} + +func TestCowGetters(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{{addr, basics.CreatableIndex(aidx), basics.AppCreatable}}) + + round := basics.Round(1234) + c.mods.Hdr.Round = round + ts := int64(11223344) + c.mods.PrevTimestamp = ts + + a.Equal(round, c.round()) + a.Equal(ts, c.prevTimestamp()) +} + +func TestCowGet(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{{addr, basics.CreatableIndex(aidx), basics.AppCreatable}}) + + addr1 := ledgertesting.RandomAddress() + bre := basics.AccountData{MicroAlgos: basics.MicroAlgos{Raw: 100}} + c.mods.Accts.Upsert(addr1, bre) + + bra, err := c.Get(addr1, true) + a.NoError(err) + a.Equal(bre, bra) + + bra, err = c.Get(addr1, false) + a.NoError(err) + a.Equal(bre, bra) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.Get(ledgertesting.RandomAddress(), true) }) +} + +func TestCowGetKey(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{{addr, basics.CreatableIndex(aidx), basics.AppCreatable}}) + + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, + } + _, ok, err := c.GetKey(addr, aidx, true, "gkey", 0) + a.Error(err) + a.False(ok) + a.Contains(err.Error(), "cannot fetch key") + + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: {storagePtr{aidx, true}: &storageDelta{action: allocAction}}, + } + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) + a.NoError(err) + a.False(ok) + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) + a.NoError(err) + a.False(ok) + + tv := basics.TealValue{Type: basics.TealUintType, Uint: 1} + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, true}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{"gkey": valueDelta{new: tv, newExists: false}}, + }, + }, + } + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) + a.NoError(err) + a.False(ok) + + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, true}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{"gkey": valueDelta{new: tv, newExists: true}}, + }, + }, + } + val, ok, err := c.GetKey(addr, aidx, true, "gkey", 0) + a.NoError(err) + a.True(ok) + a.Equal(tv, val) + + // check local + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, false}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{"lkey": valueDelta{new: tv, newExists: true}}, + }, + }, + } + + val, ok, err = c.GetKey(addr, aidx, false, "lkey", 0) + a.NoError(err) + a.True(ok) + a.Equal(tv, val) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.GetKey(ledgertesting.RandomAddress(), aidx, false, "lkey", 0) }) + a.Panics(func() { c.GetKey(addr, aidx+1, false, "lkey", 0) }) +} + +func TestCowSetKey(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{ + {addr, basics.CreatableIndex(aidx), basics.AppCreatable}, + }) + + key := strings.Repeat("key", 100) + val := "val" + tv := basics.TealValue{Type: basics.TealBytesType, Bytes: val} + err := c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "key too long") + + key = "key" + val = strings.Repeat("val", 100) + tv = basics.TealValue{Type: basics.TealBytesType, Bytes: val} + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "value too long") + + val = "val" + tv = basics.TealValue{Type: basics.TealBytesType, Bytes: val} + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, + } + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "cannot set key") + + counts := basics.StateSchema{} + maxCounts := basics.StateSchema{} + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, true}: &storageDelta{ + action: allocAction, + kvCow: make(stateDelta), + counts: &counts, + maxCounts: &maxCounts, + }, + }, + } + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "exceeds schema bytes") + + counts = basics.StateSchema{NumUint: 1} + maxCounts = basics.StateSchema{NumByteSlice: 1} + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "exceeds schema integer") + + tv2 := basics.TealValue{Type: basics.TealUintType, Uint: 1} + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, true}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{key: valueDelta{new: tv2, newExists: true}}, + counts: &counts, + maxCounts: &maxCounts, + }, + }, + } + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.NoError(err) + + counts = basics.StateSchema{NumUint: 1} + maxCounts = basics.StateSchema{NumByteSlice: 1, NumUint: 1} + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.NoError(err) + + // check local + addr1 := ledgertesting.RandomAddress() + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr1: { + storagePtr{aidx, false}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{key: valueDelta{new: tv2, newExists: true}}, + counts: &counts, + maxCounts: &maxCounts, + }, + }, + } + err = c.SetKey(addr1, aidx, false, key, tv, 0) + a.NoError(err) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.SetKey(ledgertesting.RandomAddress(), aidx, false, key, tv, 0) }) + a.Panics(func() { c.SetKey(addr, aidx+1, false, key, tv, 0) }) +} + +func TestCowSetKeyVFuture(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{ + {addr, basics.CreatableIndex(aidx), basics.AppCreatable}, + }) + protoF := config.Consensus[protocol.ConsensusFuture] + c.proto = protoF + + key := strings.Repeat("key", 100) + val := "val" + tv := basics.TealValue{Type: basics.TealBytesType, Bytes: val} + err := c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "key too long") + + key = "key" + val = strings.Repeat("val", 100) + tv = basics.TealValue{Type: basics.TealBytesType, Bytes: val} + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "value too long") + + key = strings.Repeat("k", protoF.MaxAppKeyLen) + val = strings.Repeat("v", protoF.MaxAppSumKeyValueLens-len(key)+1) + tv = basics.TealValue{Type: basics.TealBytesType, Bytes: val} + err = c.SetKey(addr, aidx, true, key, tv, 0) + a.Error(err) + a.Contains(err.Error(), "key/value total too long") +} + +func TestCowAccountIdx(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + l := emptyLedger{} + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{ + {addr, basics.CreatableIndex(aidx), basics.AppCreatable}, + }) + c.lookupParent = &l + c.compatibilityMode = true + + key := "key" + val := "val" + + c.sdeltas = make(map[basics.Address]map[storagePtr]*storageDelta) + tv := basics.TealValue{Type: basics.TealBytesType, Bytes: val} + sd, err := c.ensureStorageDelta(addr, aidx, true, remainAllocAction, 123) + a.NoError(err) + a.Equal(uint64(0), sd.accountIdx) + + c.sdeltas = make(map[basics.Address]map[storagePtr]*storageDelta) + sd, err = c.ensureStorageDelta(addr, aidx, false, remainAllocAction, 123) + a.NoError(err) + a.Equal(uint64(123), sd.accountIdx) + + counts := basics.StateSchema{} + maxCounts := basics.StateSchema{} + for _, global := range []bool{false, true} { + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, global}: &storageDelta{ + action: allocAction, + kvCow: stateDelta{key: valueDelta{new: tv, newExists: true}}, + counts: &counts, + maxCounts: &maxCounts, + accountIdx: 123, + }, + }, + } + sd, err = c.ensureStorageDelta(addr, aidx, global, remainAllocAction, 456) + a.NoError(err) + a.Equal(uint64(123), sd.accountIdx) + } +} + +func TestCowDelKey(t *testing.T) { + partitiontest.PartitionTest(t) + + a := require.New(t) + + addr := ledgertesting.RandomAddress() + aidx := basics.AppIndex(1) + c := getCow([]modsData{ + {addr, basics.CreatableIndex(aidx), basics.AppCreatable}, + }) + + key := "key" + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, + } + err := c.DelKey(addr, aidx, true, key, 0) + a.Error(err) + a.Contains(err.Error(), "cannot del key") + + counts := basics.StateSchema{} + maxCounts := basics.StateSchema{} + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, true}: &storageDelta{ + action: allocAction, + kvCow: make(stateDelta), + counts: &counts, + maxCounts: &maxCounts, + }, + }, + } + err = c.DelKey(addr, aidx, true, key, 0) + a.NoError(err) + + c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ + addr: { + storagePtr{aidx, false}: &storageDelta{ + action: allocAction, + kvCow: make(stateDelta), + counts: &counts, + maxCounts: &maxCounts, + }, + }, + } + err = c.DelKey(addr, aidx, false, key, 0) + a.NoError(err) + + // ensure other requests go down to roundCowParent + a.Panics(func() { c.DelKey(ledgertesting.RandomAddress(), aidx, false, key, 0) }) + a.Panics(func() { c.DelKey(addr, aidx+1, false, key, 0) }) +} |