diff options
author | John Lee <64482439+algojohnlee@users.noreply.github.com> | 2021-04-28 12:32:28 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-28 12:32:28 -0400 |
commit | 219b78d98184274b4ddccff47cdd1fa8091c8dac (patch) | |
tree | 07faa9c96a48b5359eaffe3a5e9916aaef91af5b | |
parent | 304815d00b9512cf9f91dbb987fead35894676f4 (diff) | |
parent | fcfaf87fc6c219d891219f190788b2f6cc3066ed (diff) |
Merge pull request #2110 from onetechnical/onetechnical/relstable2.5.6v2.5.6-stable
go-algorand 2.5.6-stable
-rw-r--r-- | buildnumber.dat | 2 | ||||
-rw-r--r-- | config/consensus.go | 22 | ||||
-rw-r--r-- | crypto/merklearray/worker.go | 10 | ||||
-rw-r--r-- | data/transactions/logic/eval.go | 12 | ||||
-rw-r--r-- | data/transactions/logic/evalStateful_test.go | 6 | ||||
-rw-r--r-- | ledger/accountdb.go | 46 | ||||
-rw-r--r-- | ledger/acctupdates.go | 63 | ||||
-rw-r--r-- | ledger/appcow.go | 98 | ||||
-rw-r--r-- | ledger/appcow_test.go | 262 | ||||
-rw-r--r-- | ledger/applications.go | 24 | ||||
-rw-r--r-- | ledger/applications_test.go | 732 | ||||
-rw-r--r-- | ledger/cow.go | 30 | ||||
-rw-r--r-- | ledger/cow_test.go | 2 | ||||
-rw-r--r-- | ledger/eval.go | 2 | ||||
-rw-r--r-- | protocol/consensus.go | 7 |
15 files changed, 1225 insertions, 93 deletions
diff --git a/buildnumber.dat b/buildnumber.dat index 7ed6ff82d..1e8b31496 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -5 +6 diff --git a/config/consensus.go b/config/consensus.go index 8c5200e0c..bf1431c49 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -348,6 +348,9 @@ type ConsensusParams struct { // update the initial rewards rate calculation to take the reward pool minimum balance into account InitialRewardsRateCalculation bool + + // NoEmptyLocalDeltas updates how ApplyDelta.EvalDelta.LocalDeltas are stored + NoEmptyLocalDeltas bool } // PaysetCommitType enumerates possible ways for the block header to commit to @@ -872,9 +875,24 @@ func initConsensusProtocols() { v25.ApprovedUpgrades[protocol.ConsensusV26] = 140000 v24.ApprovedUpgrades[protocol.ConsensusV26] = 140000 + // v27 updates ApplyDelta.EvalDelta.LocalDeltas format + v27 := v26 + v27.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + + // Enable the ApplyDelta.EvalDelta.LocalDeltas fix + v27.NoEmptyLocalDeltas = true + + Consensus[protocol.ConsensusV27] = v27 + + // v26 can be upgraded to v27, with an update delay of 3 days + // 60279 = (3 * 24 * 60 * 60 / 4.3) + // for the sake of future manual calculations, we'll round that down + // a bit : + v26.ApprovedUpgrades[protocol.ConsensusV27] = 60000 + // ConsensusFuture is used to test features that are implemented // but not yet released in a production protocol version. - vFuture := v26 + vFuture := v27 vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} // FilterTimeout for period 0 should take a new optimized, configured value, need to revisit this later @@ -895,7 +913,7 @@ func initConsensusProtocols() { Consensus[protocol.ConsensusFuture] = vFuture } -// Global defines global Algorand protocol parameters which should not be overriden. +// Global defines global Algorand protocol parameters which should not be overridden. type Global struct { SmallLambda time.Duration // min amount of time to wait for leader's credential (i.e., time to propagate one credential) BigLambda time.Duration // max amount of time to wait for leader's proposal (i.e., time to propagate one block) diff --git a/crypto/merklearray/worker.go b/crypto/merklearray/worker.go index f890ac816..67f6aa0c5 100644 --- a/crypto/merklearray/worker.go +++ b/crypto/merklearray/worker.go @@ -25,6 +25,11 @@ import ( // workerState describes a group of goroutines processing a sequential list // of maxidx elements starting from 0. type workerState struct { + // maxidx is the total number of elements to process, and nextidx + // is the next element that a worker should process. + maxidx uint64 + nextidx uint64 + // nworkers is the number of workers that can be started. // This field gets decremented once workers are launched, // and represents the number of remaining workers that can @@ -43,11 +48,6 @@ type workerState struct { // wg tracks outstanding workers, to determine when all workers // have finished their processing. wg sync.WaitGroup - - // maxidx is the total number of elements to process, and nextidx - // is the next element that a worker should process. - maxidx uint64 - nextidx uint64 } func newWorkerState(max uint64) *workerState { diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index e3b24ec43..3a5fe8e67 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -134,9 +134,9 @@ type LedgerForLogic interface { CreatorAddress() basics.Address OptedIn(addr basics.Address, appIdx basics.AppIndex) (bool, error) - GetLocal(addr basics.Address, appIdx basics.AppIndex, key string) (value basics.TealValue, exists bool, err error) - SetLocal(addr basics.Address, key string, value basics.TealValue) error - DelLocal(addr basics.Address, key string) error + GetLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) (value basics.TealValue, exists bool, err error) + SetLocal(addr basics.Address, key string, value basics.TealValue, accountIdx uint64) error + DelLocal(addr basics.Address, key string, accountIdx uint64) error GetGlobal(appIdx basics.AppIndex, key string) (value basics.TealValue, exists bool, err error) SetGlobal(key string, value basics.TealValue) error @@ -2059,7 +2059,7 @@ func (cx *evalContext) appReadLocalKey(appIdx uint64, accountIdx uint64, key str if err != nil { return basics.TealValue{}, false, err } - return cx.Ledger.GetLocal(addr, basics.AppIndex(appIdx), key) + return cx.Ledger.GetLocal(addr, basics.AppIndex(appIdx), key, accountIdx) } // appWriteLocalKey writes value to local key/value cow @@ -2069,7 +2069,7 @@ func (cx *evalContext) appWriteLocalKey(accountIdx uint64, key string, tv basics if err != nil { return err } - return cx.Ledger.SetLocal(addr, key, tv) + return cx.Ledger.SetLocal(addr, key, tv, accountIdx) } // appDeleteLocalKey deletes a value from the key/value cow @@ -2079,7 +2079,7 @@ func (cx *evalContext) appDeleteLocalKey(accountIdx uint64, key string) error { if err != nil { return err } - return cx.Ledger.DelLocal(addr, key) + return cx.Ledger.DelLocal(addr, key, accountIdx) } func (cx *evalContext) appReadGlobalKey(foreignAppsIndex uint64, key string) (basics.TealValue, bool, error) { diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index af19c586e..d5f262b3a 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -258,7 +258,7 @@ func (l *testLedger) DelGlobal(key string) error { return nil } -func (l *testLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string) (basics.TealValue, bool, error) { +func (l *testLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) (basics.TealValue, bool, error) { if appIdx == 0 { appIdx = l.appID } @@ -285,7 +285,7 @@ func (l *testLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key s return val, ok, nil } -func (l *testLedger) SetLocal(addr basics.Address, key string, value basics.TealValue) error { +func (l *testLedger) SetLocal(addr basics.Address, key string, value basics.TealValue, accountIdx uint64) error { appIdx := l.appID br, ok := l.balances[addr] @@ -313,7 +313,7 @@ func (l *testLedger) SetLocal(addr basics.Address, key string, value basics.Teal return nil } -func (l *testLedger) DelLocal(addr basics.Address, key string) error { +func (l *testLedger) DelLocal(addr basics.Address, key string, accountIdx uint64) error { appIdx := l.appID br, ok := l.balances[addr] diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 4a1640394..2019305d0 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -114,7 +114,7 @@ var accountsResetExprs = []string{ // accountDBVersion is the database version that this binary would know how to support and how to upgrade to. // details about the content of each of the versions can be found in the upgrade functions upgradeDatabaseSchemaXXXX // and their descriptions. -var accountDBVersion = int32(4) +var accountDBVersion = int32(5) // persistedAccountData is used for representing a single account stored on the disk. In addition to the // basics.AccountData, it also stores complete referencing information used to maintain the base accounts @@ -631,6 +631,50 @@ func accountsAddNormalizedBalance(tx *sql.Tx, proto config.ConsensusParams) erro return rows.Err() } +// removeEmptyAccountData removes empty AccountData msgp-encoded entries from accountbase table +// and optionally returns list of addresses that were eliminated +func removeEmptyAccountData(tx *sql.Tx, queryAddresses bool) (num int64, addresses []basics.Address, err error) { + if queryAddresses { + rows, err := tx.Query("SELECT address FROM accountbase where length(data) = 1 and data = x'80'") // empty AccountData is 0x80 + if err != nil { + return 0, nil, err + } + defer rows.Close() + + for rows.Next() { + var addrbuf []byte + err = rows.Scan(&addrbuf) + if err != nil { + return 0, nil, err + } + var addr basics.Address + if len(addrbuf) != len(addr) { + err = fmt.Errorf("Account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return 0, nil, err + } + copy(addr[:], addrbuf) + addresses = append(addresses, addr) + } + + // if the above loop was abrupted by an error, test it now. + if err = rows.Err(); err != nil { + return 0, nil, err + } + } + + result, err := tx.Exec("DELETE from accountbase where length(data) = 1 and data = x'80'") + if err != nil { + return 0, nil, err + } + num, err = result.RowsAffected() + if err != nil { + // something wrong on getting rows count but data deleted, ignore the error + num = int64(len(addresses)) + err = nil + } + return num, addresses, err +} + // accountDataToOnline returns the part of the AccountData that matters // for online accounts (to answer top-N queries). We store a subset of // the full AccountData because we need to store a large number of these diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 099d43724..d68a7a567 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1069,6 +1069,12 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 3 : %v", err) return 0, err } + case 4: + dbVersion, err = au.upgradeDatabaseSchema4(ctx, tx) + if err != nil { + au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 4 : %v", err) + return 0, err + } default: return 0, fmt.Errorf("accountsInitialize unable to upgrade database from schema version %d", dbVersion) } @@ -1324,6 +1330,63 @@ func (au *accountUpdates) upgradeDatabaseSchema3(ctx context.Context, tx *sql.Tx return 4, nil } +// upgradeDatabaseSchema4 does not change the schema but migrates data: +// remove empty AccountData entries from accountbase table +func (au *accountUpdates) upgradeDatabaseSchema4(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { + + queryAddresses := au.catchpointInterval != 0 + numDeleted, addresses, err := removeEmptyAccountData(tx, queryAddresses) + if err != nil { + return 0, err + } + + if queryAddresses && len(addresses) > 0 { + mc, err := makeMerkleCommitter(tx, false) + if err != nil { + // at this point record deleted and DB is pruned for account data + // if hash deletion fails just log it and do not about startup + au.log.Errorf("upgradeDatabaseSchema4: failed to create merkle committer: %v", err) + goto done + } + trie, err := merkletrie.MakeTrie(mc, trieMemoryConfig) + if err != nil { + au.log.Errorf("upgradeDatabaseSchema4: failed to create merkle trie: %v", err) + goto done + } + + var totalHashesDeleted int + for _, addr := range addresses { + hash := accountHashBuilder(addr, basics.AccountData{}, []byte{0x80}) + deleted, err := trie.Delete(hash) + if err != nil { + au.log.Errorf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v: %v", hex.EncodeToString(hash), addr, err) + } else { + if !deleted { + au.log.Warnf("upgradeDatabaseSchema4: failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(hash), addr) + } else { + totalHashesDeleted++ + } + } + } + + if _, err = trie.Commit(); err != nil { + au.log.Errorf("upgradeDatabaseSchema4: failed to commit changes to merkle trie: %v", err) + } + + au.log.Infof("upgradeDatabaseSchema4: deleted %d hashes", totalHashesDeleted) + } + +done: + au.log.Infof("upgradeDatabaseSchema4: deleted %d rows", numDeleted) + + // update version + _, err = db.SetUserVersion(ctx, tx, 5) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to update database schema version from 4 to 5: %v", err) + } + return 5, nil +} + // deleteStoredCatchpoints iterates over the storedcatchpoints table and deletes all the files stored on disk. // once all the files have been deleted, it would go ahead and remove the entries from the table. func (au *accountUpdates) deleteStoredCatchpoints(ctx context.Context, dbQueries *accountsDbQueries) (err error) { diff --git a/ledger/appcow.go b/ledger/appcow.go index 0286bb612..f0904a6cc 100644 --- a/ledger/appcow.go +++ b/ledger/appcow.go @@ -92,10 +92,15 @@ type storageDelta struct { kvCow stateDelta counts, maxCounts *basics.StateSchema + + // account index for an address that was first referenced as in app_local_get/app_local_put/app_local_del + // this is for backward compatibility with original implementation of applications + // it is set only once on storageDelta creation and used only for local delta generation + accountIdx uint64 } // ensureStorageDelta finds existing or allocate a new storageDelta for given {addr, aidx, global} -func (cb *roundCowState) ensureStorageDelta(addr basics.Address, aidx basics.AppIndex, global bool, defaultAction storageAction) (*storageDelta, error) { +func (cb *roundCowState) ensureStorageDelta(addr basics.Address, aidx basics.AppIndex, global bool, defaultAction storageAction, accountIdx uint64) (*storageDelta, error) { // If we already have a storageDelta, return it aapp := storagePtr{aidx, global} lsd, ok := cb.sdeltas[addr][aapp] @@ -122,6 +127,17 @@ func (cb *roundCowState) ensureStorageDelta(addr basics.Address, aidx basics.App maxCounts: &maxCounts, } + if cb.compatibilityMode && !global { + lsd.accountIdx = accountIdx + + // if there was previous getKey call for this app and address, use that index instead + if s, ok := cb.compatibilityGetKeyCache[addr]; ok { + if idx, ok := s[aapp]; ok { + lsd.accountIdx = idx + } + } + } + _, ok = cb.sdeltas[addr] if !ok { cb.sdeltas[addr] = make(map[storagePtr]*storageDelta) @@ -217,7 +233,7 @@ func (cb *roundCowState) Allocate(addr basics.Address, aidx basics.AppIndex, glo return err } - lsd, err := cb.ensureStorageDelta(addr, aidx, global, allocAction) + lsd, err := cb.ensureStorageDelta(addr, aidx, global, allocAction, 0) if err != nil { return err } @@ -240,7 +256,7 @@ func (cb *roundCowState) Deallocate(addr basics.Address, aidx basics.AppIndex, g return err } - lsd, err := cb.ensureStorageDelta(addr, aidx, global, deallocAction) + lsd, err := cb.ensureStorageDelta(addr, aidx, global, deallocAction, 0) if err != nil { return err } @@ -253,13 +269,13 @@ func (cb *roundCowState) Deallocate(addr basics.Address, aidx basics.AppIndex, g } // GetKey looks for a key in {addr, aidx, global} storage -func (cb *roundCowState) GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { - return cb.getKey(addr, aidx, global, key) +func (cb *roundCowState) GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { + return cb.getKey(addr, aidx, global, key, accountIdx) } // getKey looks for a key in {addr, aidx, global} storage // This is hierarchical lookup: if the key not in this cow cache, then request parent and all way down to ledger -func (cb *roundCowState) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { +func (cb *roundCowState) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { // Check that account has allocated storage allocated, err := cb.allocated(addr, aidx, global) if err != nil { @@ -287,13 +303,28 @@ func (cb *roundCowState) getKey(addr basics.Address, aidx basics.AppIndex, globa } } + if cb.compatibilityMode && !global { + // if fetching a key first time for this app, + // cache account index, and use it later on lsd allocation + s, ok := cb.compatibilityGetKeyCache[addr] + if !ok { + s = map[storagePtr]uint64{{aidx, global}: accountIdx} + cb.compatibilityGetKeyCache[addr] = s + } else { + if _, ok := s[storagePtr{aidx, global}]; !ok { + s[storagePtr{aidx, global}] = accountIdx + cb.compatibilityGetKeyCache[addr] = s + } + } + } + // At this point, we know we're allocated, and we don't have a delta, // so we should check our parent. - return cb.lookupParent.getKey(addr, aidx, global, key) + return cb.lookupParent.getKey(addr, aidx, global, key, accountIdx) } // SetKey creates a new key-value in {addr, aidx, global} storage -func (cb *roundCowState) SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue) error { +func (cb *roundCowState) SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue, accountIdx uint64) error { // Enforce maximum key length if len(key) > cb.proto.MaxAppKeyLen { return fmt.Errorf("key too long: length was %d, maximum is %d", len(key), cb.proto.MaxAppKeyLen) @@ -316,13 +347,13 @@ func (cb *roundCowState) SetKey(addr basics.Address, aidx basics.AppIndex, globa } // Fetch the old value + presence so we know how to update - oldValue, oldOk, err := cb.GetKey(addr, aidx, global, key) + oldValue, oldOk, err := cb.GetKey(addr, aidx, global, key, accountIdx) if err != nil { return err } // Write the value delta associated with this key/value - lsd, err := cb.ensureStorageDelta(addr, aidx, global, remainAllocAction) + lsd, err := cb.ensureStorageDelta(addr, aidx, global, remainAllocAction, accountIdx) if err != nil { return err } @@ -347,7 +378,7 @@ func (cb *roundCowState) SetKey(addr basics.Address, aidx basics.AppIndex, globa } // DelKey removes a key from {addr, aidx, global} storage -func (cb *roundCowState) DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) error { +func (cb *roundCowState) DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) error { // Check that account has allocated storage allocated, err := cb.allocated(addr, aidx, global) if err != nil { @@ -359,13 +390,13 @@ func (cb *roundCowState) DelKey(addr basics.Address, aidx basics.AppIndex, globa } // Fetch the old value + presence so we know how to update counts - oldValue, oldOk, err := cb.GetKey(addr, aidx, global, key) + oldValue, oldOk, err := cb.GetKey(addr, aidx, global, key, accountIdx) if err != nil { return err } // Write the value delta associated with deleting this key - lsd, err := cb.ensureStorageDelta(addr, aidx, global, remainAllocAction) + lsd, err := cb.ensureStorageDelta(addr, aidx, global, remainAllocAction, accountIdx) if err != nil { return nil } @@ -457,17 +488,29 @@ func (cb *roundCowState) BuildEvalDelta(aidx basics.AppIndex, txn *transactions. if evalDelta.LocalDeltas == nil { evalDelta.LocalDeltas = make(map[uint64]basics.StateDelta) } + // It is impossible for there to be more than one local delta for // a particular (address, app ID) in sdeltas, because the appAddr // type consists only of (address, appID, global=false). So if // IndexByAddress is deterministic (and it is), there is no need // to check for duplicates here. var addrOffset uint64 - addrOffset, err = txn.IndexByAddress(addr, txn.Sender) - if err != nil { - return basics.EvalDelta{}, err + if cb.compatibilityMode { + addrOffset = sdelta.accountIdx + } else { + addrOffset, err = txn.IndexByAddress(addr, txn.Sender) + if err != nil { + return basics.EvalDelta{}, err + } + } + + d := sdelta.kvCow.serialize() + // noEmptyDeltas restricts prodicing empty local deltas in general + // but allows it for a period of time when a buggy version was live + noEmptyDeltas := cb.proto.NoEmptyLocalDeltas || (cb.mods.Hdr.CurrentProtocol == protocol.ConsensusV24) && (cb.mods.Hdr.NextProtocol != protocol.ConsensusV26) + if !noEmptyDeltas || len(d) != 0 { + evalDelta.LocalDeltas[addrOffset] = d } - evalDelta.LocalDeltas[addrOffset] = sdelta.kvCow.serialize() } } } @@ -558,9 +601,12 @@ func applyStorageDelta(data basics.AccountData, aapp storagePtr, store *storageD // duplicate code in branches is proven to be a bit faster than // having basics.AppParams and basics.AppLocalState under a common interface with additional loops and type assertions if aapp.global { - owned := make(map[basics.AppIndex]basics.AppParams, len(data.AppParams)) - for k, v := range data.AppParams { - owned[k] = v + var owned map[basics.AppIndex]basics.AppParams + if len(data.AppParams) > 0 { + owned = make(map[basics.AppIndex]basics.AppParams, len(data.AppParams)) + for k, v := range data.AppParams { + owned[k] = v + } } switch store.action { @@ -596,9 +642,12 @@ func applyStorageDelta(data basics.AccountData, aapp storagePtr, store *storageD data.AppParams = owned } else { - owned := make(map[basics.AppIndex]basics.AppLocalState, len(data.AppLocalStates)) - for k, v := range data.AppLocalStates { - owned[k] = v + var owned map[basics.AppIndex]basics.AppLocalState + if len(data.AppLocalStates) > 0 { + owned = make(map[basics.AppIndex]basics.AppLocalState, len(data.AppLocalStates)) + for k, v := range data.AppLocalStates { + owned[k] = v + } } switch store.action { @@ -606,7 +655,8 @@ func applyStorageDelta(data basics.AccountData, aapp storagePtr, store *storageD delete(owned, aapp.aidx) case allocAction, remainAllocAction: // note: these should always exist because they were - // at least preceded by a call to Put? + // at least preceded by a call to Put (opting in), + // or the account has opted in before and local states are pre-allocated states, ok := owned[aapp.aidx] if !ok { return basics.AccountData{}, fmt.Errorf("could not find existing states for %v", aapp.aidx) diff --git a/ledger/appcow_test.go b/ledger/appcow_test.go index 32e6a14c1..058377d02 100644 --- a/ledger/appcow_test.go +++ b/ledger/appcow_test.go @@ -73,7 +73,7 @@ func (ml *emptyLedger) allocated(addr basics.Address, aidx basics.AppIndex, glob return false, nil } -func (ml *emptyLedger) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { +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 } @@ -269,7 +269,7 @@ func TestCowStorage(t *testing.T) { 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) + err := cow.SetKey(addr, sptr.aidx, sptr.global, rkey, rval, 0) if actuallyAllocated { require.NoError(t, err) err = st.set(aapp, rkey, rval) @@ -284,7 +284,7 @@ func TestCowStorage(t *testing.T) { if rand.Float32() < 0.25 { actuallyAllocated := st.allocated(aapp) rkey := allKeys[rand.Intn(len(allKeys))] - err := cow.DelKey(addr, sptr.aidx, sptr.global, rkey) + err := cow.DelKey(addr, sptr.aidx, sptr.global, rkey, 0) if actuallyAllocated { require.NoError(t, err) err = st.del(aapp, rkey) @@ -326,7 +326,7 @@ func TestCowStorage(t *testing.T) { tval, tok, err := st.get(aapp, key) require.NoError(t, err) - cval, cok, err := cow.GetKey(addr, sptr.aidx, sptr.global, key) + 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) @@ -407,7 +407,11 @@ func TestCowBuildDelta(t *testing.T) { a.Contains(err.Error(), "could not find offset") 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( @@ -418,6 +422,19 @@ func TestCowBuildDelta(t *testing.T) { 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( + basics.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{ @@ -444,6 +461,139 @@ func TestCowBuildDelta(t *testing.T) { }, 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( + basics.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( + basics.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( + basics.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( + basics.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) { @@ -756,6 +906,18 @@ func TestApplyStorageDelta(t *testing.T) { 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) { @@ -855,7 +1017,7 @@ func TestCowGetKey(t *testing.T) { c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, } - _, ok, err := c.GetKey(addr, aidx, true, "gkey") + _, ok, err := c.GetKey(addr, aidx, true, "gkey", 0) a.Error(err) a.False(ok) a.Contains(err.Error(), "cannot fetch key") @@ -863,10 +1025,10 @@ func TestCowGetKey(t *testing.T) { c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ addr: {storagePtr{aidx, true}: &storageDelta{action: allocAction}}, } - _, ok, err = c.GetKey(addr, aidx, true, "gkey") + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) a.NoError(err) a.False(ok) - _, ok, err = c.GetKey(addr, aidx, true, "gkey") + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) a.NoError(err) a.False(ok) @@ -879,7 +1041,7 @@ func TestCowGetKey(t *testing.T) { }, }, } - _, ok, err = c.GetKey(addr, aidx, true, "gkey") + _, ok, err = c.GetKey(addr, aidx, true, "gkey", 0) a.NoError(err) a.False(ok) @@ -891,7 +1053,7 @@ func TestCowGetKey(t *testing.T) { }, }, } - val, ok, err := c.GetKey(addr, aidx, true, "gkey") + val, ok, err := c.GetKey(addr, aidx, true, "gkey", 0) a.NoError(err) a.True(ok) a.Equal(tv, val) @@ -906,14 +1068,14 @@ func TestCowGetKey(t *testing.T) { }, } - val, ok, err = c.GetKey(addr, aidx, false, "lkey") + 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(getRandomAddress(a), aidx, false, "lkey") }) - a.Panics(func() { c.GetKey(addr, aidx+1, false, "lkey") }) + a.Panics(func() { c.GetKey(getRandomAddress(a), aidx, false, "lkey", 0) }) + a.Panics(func() { c.GetKey(addr, aidx+1, false, "lkey", 0) }) } func TestCowSetKey(t *testing.T) { @@ -928,14 +1090,14 @@ func TestCowSetKey(t *testing.T) { key := strings.Repeat("key", 100) val := "val" tv := basics.TealValue{Type: basics.TealBytesType, Bytes: val} - err := c.SetKey(addr, aidx, true, key, tv) + 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) + err = c.SetKey(addr, aidx, true, key, tv, 0) a.Error(err) a.Contains(err.Error(), "value too long") @@ -944,7 +1106,7 @@ func TestCowSetKey(t *testing.T) { c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, } - err = c.SetKey(addr, aidx, true, key, tv) + err = c.SetKey(addr, aidx, true, key, tv, 0) a.Error(err) a.Contains(err.Error(), "cannot set key") @@ -960,13 +1122,13 @@ func TestCowSetKey(t *testing.T) { }, }, } - err = c.SetKey(addr, aidx, true, key, tv) + 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) + err = c.SetKey(addr, aidx, true, key, tv, 0) a.Error(err) a.Contains(err.Error(), "exceeds schema integer") @@ -981,12 +1143,12 @@ func TestCowSetKey(t *testing.T) { }, }, } - err = c.SetKey(addr, aidx, true, key, tv) + 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) + err = c.SetKey(addr, aidx, true, key, tv, 0) a.NoError(err) // check local @@ -1001,12 +1163,58 @@ func TestCowSetKey(t *testing.T) { }, }, } - err = c.SetKey(addr1, aidx, false, key, tv) + err = c.SetKey(addr1, aidx, false, key, tv, 0) a.NoError(err) // ensure other requests go down to roundCowParent - a.Panics(func() { c.SetKey(getRandomAddress(a), aidx, false, key, tv) }) - a.Panics(func() { c.SetKey(addr, aidx+1, false, key, tv) }) + a.Panics(func() { c.SetKey(getRandomAddress(a), aidx, false, key, tv, 0) }) + a.Panics(func() { c.SetKey(addr, aidx+1, false, key, tv, 0) }) +} + +func TestCowAccountIdx(t *testing.T) { + a := require.New(t) + + l := emptyLedger{} + addr := getRandomAddress(a) + 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) { @@ -1022,7 +1230,7 @@ func TestCowDelKey(t *testing.T) { c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ addr: {storagePtr{aidx, true}: &storageDelta{action: deallocAction}}, } - err := c.DelKey(addr, aidx, true, key) + err := c.DelKey(addr, aidx, true, key, 0) a.Error(err) a.Contains(err.Error(), "cannot del key") @@ -1038,7 +1246,7 @@ func TestCowDelKey(t *testing.T) { }, }, } - err = c.DelKey(addr, aidx, true, key) + err = c.DelKey(addr, aidx, true, key, 0) a.NoError(err) c.sdeltas = map[basics.Address]map[storagePtr]*storageDelta{ @@ -1051,10 +1259,10 @@ func TestCowDelKey(t *testing.T) { }, }, } - err = c.DelKey(addr, aidx, false, key) + err = c.DelKey(addr, aidx, false, key, 0) a.NoError(err) // ensure other requests go down to roundCowParent - a.Panics(func() { c.DelKey(getRandomAddress(a), aidx, false, key) }) - a.Panics(func() { c.DelKey(addr, aidx+1, false, key) }) + a.Panics(func() { c.DelKey(getRandomAddress(a), aidx, false, key, 0) }) + a.Panics(func() { c.DelKey(addr, aidx+1, false, key, 0) }) } diff --git a/ledger/applications.go b/ledger/applications.go index 9dee2b4e6..5f1d9bca4 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -33,11 +33,11 @@ type logicLedger struct { type cowForLogicLedger interface { Get(addr basics.Address, withPendingRewards bool) (basics.AccountData, error) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) - GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) + GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) BuildEvalDelta(aidx basics.AppIndex, txn *transactions.Transaction) (basics.EvalDelta, error) - SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue) error - DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) error + SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue, accountIdx uint64) error + DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) error round() basics.Round prevTimestamp() int64 @@ -153,19 +153,19 @@ func (al *logicLedger) OptedIn(addr basics.Address, appIdx basics.AppIndex) (boo return al.cow.allocated(addr, appIdx, false) } -func (al *logicLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string) (basics.TealValue, bool, error) { +func (al *logicLedger) GetLocal(addr basics.Address, appIdx basics.AppIndex, key string, accountIdx uint64) (basics.TealValue, bool, error) { if appIdx == basics.AppIndex(0) { appIdx = al.aidx } - return al.cow.GetKey(addr, appIdx, false, key) + return al.cow.GetKey(addr, appIdx, false, key, accountIdx) } -func (al *logicLedger) SetLocal(addr basics.Address, key string, value basics.TealValue) error { - return al.cow.SetKey(addr, al.aidx, false, key, value) +func (al *logicLedger) SetLocal(addr basics.Address, key string, value basics.TealValue, accountIdx uint64) error { + return al.cow.SetKey(addr, al.aidx, false, key, value, accountIdx) } -func (al *logicLedger) DelLocal(addr basics.Address, key string) error { - return al.cow.DelKey(addr, al.aidx, false, key) +func (al *logicLedger) DelLocal(addr basics.Address, key string, accountIdx uint64) error { + return al.cow.DelKey(addr, al.aidx, false, key, accountIdx) } func (al *logicLedger) fetchAppCreator(appIdx basics.AppIndex) (basics.Address, error) { @@ -189,15 +189,15 @@ func (al *logicLedger) GetGlobal(appIdx basics.AppIndex, key string) (basics.Tea if err != nil { return basics.TealValue{}, false, err } - return al.cow.GetKey(addr, appIdx, true, key) + return al.cow.GetKey(addr, appIdx, true, key, 0) } func (al *logicLedger) SetGlobal(key string, value basics.TealValue) error { - return al.cow.SetKey(al.creator, al.aidx, true, key, value) + return al.cow.SetKey(al.creator, al.aidx, true, key, value, 0) } func (al *logicLedger) DelGlobal(key string) error { - return al.cow.DelKey(al.creator, al.aidx, true, key) + return al.cow.DelKey(al.creator, al.aidx, true, key, 0) } func (al *logicLedger) GetDelta(txn *transactions.Transaction) (evalDelta basics.EvalDelta, err error) { diff --git a/ledger/applications_test.go b/ledger/applications_test.go index 0e827940e..87d277ffd 100644 --- a/ledger/applications_test.go +++ b/ledger/applications_test.go @@ -74,7 +74,7 @@ func (c *mockCowForLogicLedger) GetCreator(cidx basics.CreatableIndex, ctype bas return addr, found, nil } -func (c *mockCowForLogicLedger) GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { +func (c *mockCowForLogicLedger) GetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { kv, ok := c.stores[storeLocator{addr, aidx, global}] if !ok { return basics.TealValue{}, false, fmt.Errorf("no store for (%s %d %v) in mock cow", addr.String(), aidx, global) @@ -87,7 +87,7 @@ func (c *mockCowForLogicLedger) BuildEvalDelta(aidx basics.AppIndex, txn *transa return basics.EvalDelta{}, nil } -func (c *mockCowForLogicLedger) SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue) error { +func (c *mockCowForLogicLedger) SetKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, value basics.TealValue, accountIdx uint64) error { kv, ok := c.stores[storeLocator{addr, aidx, global}] if !ok { return fmt.Errorf("no store for (%s %d %v) in mock cow", addr.String(), aidx, global) @@ -97,7 +97,7 @@ func (c *mockCowForLogicLedger) SetKey(addr basics.Address, aidx basics.AppIndex return nil } -func (c *mockCowForLogicLedger) DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) error { +func (c *mockCowForLogicLedger) DelKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) error { kv, ok := c.stores[storeLocator{addr, aidx, global}] if !ok { return fmt.Errorf("no store for (%s %d %v) in mock cow", addr.String(), aidx, global) @@ -277,7 +277,7 @@ func TestLogicLedgerGetKey(t *testing.T) { // check local c.stores = map[storeLocator]basics.TealKeyValue{{addr, aidx, false}: {"lkey": tv}} - val, ok, err = l.GetLocal(addr, aidx, "lkey") + val, ok, err = l.GetLocal(addr, aidx, "lkey", 0) a.NoError(err) a.True(ok) a.Equal(tv, val) @@ -307,7 +307,7 @@ func TestLogicLedgerSetKey(t *testing.T) { // check local c.stores = map[storeLocator]basics.TealKeyValue{{addr, aidx, false}: {"lkey": tv}} - err = l.SetLocal(addr, "lkey", tv2) + err = l.SetLocal(addr, "lkey", tv2, 0) a.NoError(err) } @@ -334,7 +334,7 @@ func TestLogicLedgerDelKey(t *testing.T) { addr1 := getRandomAddress(a) c.stores = map[storeLocator]basics.TealKeyValue{{addr1, aidx, false}: {"lkey": tv}} - err = l.DelLocal(addr1, "lkey") + err = l.DelLocal(addr1, "lkey", 0) a.NoError(err) } @@ -567,3 +567,723 @@ return` }) a.NoError(err) } + +func TestAppAccountDelta(t *testing.T) { + a := require.New(t) + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +// if no args then write local +// otherwise check args and write local or global +txn NumAppArgs +int 0 +== +bnz writelocal +txna ApplicationArgs 0 +byte "local" +== +bnz writelocal +txna ApplicationArgs 0 +byte "local1" +== +bnz writelocal1 +txna ApplicationArgs 0 +byte "global" +== +bnz writeglobal +int 0 +return +writelocal: +int 0 +byte "lk" +byte "local" +app_local_put +b success +writelocal1: +int 0 +byte "lk1" +byte "local1" +app_local_put +b success +writeglobal: +byte "gk" +byte "global" +app_global_put +success: +int 1 +return` + + ops, err := logic.AssembleString(source) + a.NoError(err) + a.Greater(len(ops.Program), 1) + program := ops.Program + + proto := config.Consensus[protocol.ConsensusCurrentVersion] + genesisInitState, initKeys := testGenerateInitState(t, protocol.ConsensusCurrentVersion) + + creator, err := basics.UnmarshalChecksumAddress("3LN5DBFC2UTPD265LQDP3LMTLGZCQ5M3JV7XTVTGRH5CKSVNQVDFPN6FG4") + a.NoError(err) + userLocal, err := basics.UnmarshalChecksumAddress("UL5C6SRVLOROSB5FGAE6TY34VXPXVR7GNIELUB3DD5KTA4VT6JGOZ6WFAY") + a.NoError(err) + + a.Contains(genesisInitState.Accounts, creator) + a.Contains(genesisInitState.Accounts, userLocal) + + cfg := config.GetDefaultLocal() + l, err := OpenLedger(logging.Base(), t.Name(), true, genesisInitState, cfg) + a.NoError(err) + defer l.Close() + + genesisID := t.Name() + txHeader := transactions.Header{ + Sender: creator, + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: l.Latest() + 1, + LastValid: l.Latest() + 10, + GenesisID: genesisID, + GenesisHash: genesisInitState.GenesisHash, + } + + // create application + approvalProgram := program + clearStateProgram := []byte("\x02") // empty + appCreateFields := transactions.ApplicationCallTxnFields{ + ApprovalProgram: approvalProgram, + ClearStateProgram: clearStateProgram, + GlobalStateSchema: basics.StateSchema{NumByteSlice: 4}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + } + appCreate := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCreateFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCreate, transactions.ApplyData{}) + a.NoError(err) + + appIdx := basics.AppIndex(1) // first tnx => idx = 1 + + // opt-in, write to local + txHeader.Sender = userLocal + appCallFields := transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.OptInOC, + ApplicationID: appIdx, + } + appCall := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, transactions.ApplyData{ + EvalDelta: basics.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{0: {"lk": basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "local", + }}}, + }, + }) + a.NoError(err) + + txHeader.Sender = userLocal + appCallFields = transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.NoOpOC, + ApplicationID: appIdx, + } + appCall = transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, transactions.ApplyData{}) + a.NoError(err) + + // save data into DB and write into local state + l.accts.accountsWriting.Add(1) + l.accts.commitRound(3, 0, 0) + l.reloadLedger() + + // check first write + blk, err := l.Block(2) + a.NoError(err) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas, uint64(0)) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0], "lk") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0]["lk"].Bytes, "local") + expectedAD := transactions.ApplyData{} + dec, err := hex.DecodeString("81a2647481a26c64810081a26c6b82a2617401a26273a56c6f63616c") + a.NoError(err) + err = protocol.Decode(dec, &expectedAD) + a.NoError(err) + a.Equal(expectedAD, blk.Payset[0].ApplyData) + + // check repeated write + blk, err = l.Block(3) + a.NoError(err) + a.Empty(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas) + expectedAD = transactions.ApplyData{} + dec, err = hex.DecodeString("80") + a.NoError(err) + err = protocol.Decode(dec, &expectedAD) + a.NoError(err) + a.Equal(expectedAD, blk.Payset[0].ApplyData) + + txHeader.Sender = creator + appCallFields = transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.NoOpOC, + ApplicationID: appIdx, + ApplicationArgs: [][]byte{[]byte("global")}, + } + appCall = transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, + transactions.ApplyData{EvalDelta: basics.EvalDelta{ + GlobalDelta: basics.StateDelta{"gk": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "global"}}}, + }) + a.NoError(err) + + // repeat writing into global state + txHeader.Lease = [32]byte{1} + appCall = transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, transactions.ApplyData{}) + a.NoError(err) + + // save data into DB + l.accts.accountsWriting.Add(1) + l.accts.commitRound(2, 3, 0) + l.reloadLedger() + + // check first write + blk, err = l.Block(4) + a.NoError(err) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta, "gk") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta["gk"].Bytes, "global") + expectedAD = transactions.ApplyData{} + dec, err = hex.DecodeString("81a2647481a2676481a2676b82a2617401a26273a6676c6f62616c") + a.NoError(err) + err = protocol.Decode(dec, &expectedAD) + a.NoError(err) + a.Equal(expectedAD, blk.Payset[0].ApplyData) + + // check repeated write + blk, err = l.Block(5) + a.NoError(err) + a.NotContains(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta, "gk") + expectedAD = transactions.ApplyData{} + dec, err = hex.DecodeString("80") + a.NoError(err) + err = protocol.Decode(dec, &expectedAD) + a.NoError(err) + a.Equal(expectedAD, blk.Payset[0].ApplyData) + + // check same key update in the same block + txHeader.Sender = userLocal + txHeader.Lease = [32]byte{2} + appCallFields = transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.NoOpOC, + ApplicationID: appIdx, + ApplicationArgs: [][]byte{[]byte("local1")}, + } + appCall1 := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + + txHeader.Lease = [32]byte{3} + appCall2 := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + + stx1 := sign(initKeys, appCall1) + stx2 := sign(initKeys, appCall2) + + blk = makeNewEmptyBlock(t, l, genesisID, genesisInitState.Accounts) + ad1 := transactions.ApplyData{ + EvalDelta: basics.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{0: {"lk1": basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "local1", + }}}, + }, + } + txib1, err := blk.EncodeSignedTxn(stx1, ad1) + a.NoError(err) + txib2, err := blk.EncodeSignedTxn(stx2, transactions.ApplyData{}) + a.NoError(err) + blk.TxnCounter = blk.TxnCounter + 2 + blk.Payset = append(blk.Payset, txib1, txib2) + blk.TxnRoot, err = blk.PaysetCommit() + a.NoError(err) + err = l.appendUnvalidated(blk) + a.NoError(err) + + // first txn has delta + blk, err = l.Block(6) + a.NoError(err) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas, uint64(0)) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0], "lk1") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0]["lk1"].Bytes, "local1") + expectedAD = transactions.ApplyData{} + dec, err = hex.DecodeString("81a2647481a26c64810081a36c6b3182a2617401a26273a66c6f63616c31") + a.NoError(err) + err = protocol.Decode(dec, &expectedAD) + a.NoError(err) + a.Equal(expectedAD, blk.Payset[0].ApplyData) + + // second txn does not have delta (same key/value update) + a.Empty(blk.Payset[1].ApplyData.EvalDelta.LocalDeltas) + a.Equal(transactions.ApplyData{}, blk.Payset[1].ApplyData) +} + +func TestAppEmptyAccountsLocal(t *testing.T) { + a := require.New(t) + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +int 0 +byte "lk" +byte "local" +app_local_put +success: +int 1 +return` + + ops, err := logic.AssembleString(source) + a.NoError(err) + a.Greater(len(ops.Program), 1) + program := ops.Program + + proto := config.Consensus[protocol.ConsensusCurrentVersion] + genesisInitState, initKeys := testGenerateInitState(t, protocol.ConsensusCurrentVersion) + + creator, err := basics.UnmarshalChecksumAddress("3LN5DBFC2UTPD265LQDP3LMTLGZCQ5M3JV7XTVTGRH5CKSVNQVDFPN6FG4") + a.NoError(err) + userLocal, err := basics.UnmarshalChecksumAddress("UL5C6SRVLOROSB5FGAE6TY34VXPXVR7GNIELUB3DD5KTA4VT6JGOZ6WFAY") + a.NoError(err) + + a.Contains(genesisInitState.Accounts, creator) + a.Contains(genesisInitState.Accounts, userLocal) + + cfg := config.GetDefaultLocal() + l, err := OpenLedger(logging.Base(), t.Name(), true, genesisInitState, cfg) + a.NoError(err) + defer l.Close() + + genesisID := t.Name() + txHeader := transactions.Header{ + Sender: creator, + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: l.Latest() + 1, + LastValid: l.Latest() + 10, + GenesisID: genesisID, + GenesisHash: genesisInitState.GenesisHash, + } + + // create application + approvalProgram := program + clearStateProgram := []byte("\x02") // empty + appCreateFields := transactions.ApplicationCallTxnFields{ + ApprovalProgram: approvalProgram, + ClearStateProgram: clearStateProgram, + GlobalStateSchema: basics.StateSchema{NumByteSlice: 4}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + } + appCreate := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCreateFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCreate, transactions.ApplyData{}) + a.NoError(err) + + appIdx := basics.AppIndex(1) // first tnx => idx = 1 + + // opt-in, write to local + txHeader.Sender = userLocal + appCallFields := transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.OptInOC, + ApplicationID: appIdx, + } + appCall := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, transactions.ApplyData{ + EvalDelta: basics.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{0: {"lk": basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "local", + }}}, + }, + }) + a.NoError(err) + + // close out + txHeader.Sender = userLocal + appCallFields = transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.CloseOutOC, + ApplicationID: appIdx, + } + appCall = transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + paymentFields := transactions.PaymentTxnFields{ + Amount: basics.MicroAlgos{Raw: 0}, + Receiver: creator, + CloseRemainderTo: creator, + } + payment := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: txHeader, + PaymentTxnFields: paymentFields, + } + + data := genesisInitState.Accounts[userLocal] + balance := basics.MicroAlgos{Raw: data.MicroAlgos.Raw - txHeader.Fee.Raw*3} + stx1 := sign(initKeys, appCall) + stx2 := sign(initKeys, payment) + + blk := makeNewEmptyBlock(t, l, genesisID, genesisInitState.Accounts) + txib1, err := blk.EncodeSignedTxn(stx1, transactions.ApplyData{}) + a.NoError(err) + txib2, err := blk.EncodeSignedTxn(stx2, transactions.ApplyData{ClosingAmount: balance}) + a.NoError(err) + blk.TxnCounter = blk.TxnCounter + 2 + blk.Payset = append(blk.Payset, txib1, txib2) + blk.TxnRoot, err = blk.PaysetCommit() + a.NoError(err) + err = l.appendUnvalidated(blk) + a.NoError(err) + + // save data into DB and write into local state + l.accts.accountsWriting.Add(1) + l.accts.commitRound(3, 0, 0) + l.reloadLedger() + + // check first write + blk, err = l.Block(2) + a.NoError(err) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas, uint64(0)) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0], "lk") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[0]["lk"].Bytes, "local") + + // check close out + blk, err = l.Block(3) + a.NoError(err) + a.Empty(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas) + + pad, err := l.accts.accountsq.lookup(userLocal) + a.NoError(err) + a.Equal(basics.AccountData{}, pad.accountData) + a.Zero(pad.rowid) +} + +func TestAppEmptyAccountsGlobal(t *testing.T) { + a := require.New(t) + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +byte "gk" +byte "global" +app_global_put +success: +int 1 +return` + + ops, err := logic.AssembleString(source) + a.NoError(err) + a.Greater(len(ops.Program), 1) + program := ops.Program + + proto := config.Consensus[protocol.ConsensusCurrentVersion] + genesisInitState, initKeys := testGenerateInitState(t, protocol.ConsensusCurrentVersion) + + creator, err := basics.UnmarshalChecksumAddress("3LN5DBFC2UTPD265LQDP3LMTLGZCQ5M3JV7XTVTGRH5CKSVNQVDFPN6FG4") + a.NoError(err) + userLocal, err := basics.UnmarshalChecksumAddress("UL5C6SRVLOROSB5FGAE6TY34VXPXVR7GNIELUB3DD5KTA4VT6JGOZ6WFAY") + a.NoError(err) + + a.Contains(genesisInitState.Accounts, creator) + a.Contains(genesisInitState.Accounts, userLocal) + + cfg := config.GetDefaultLocal() + l, err := OpenLedger(logging.Base(), t.Name(), true, genesisInitState, cfg) + a.NoError(err) + defer l.Close() + + genesisID := t.Name() + txHeader := transactions.Header{ + Sender: creator, + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: l.Latest() + 1, + LastValid: l.Latest() + 10, + GenesisID: genesisID, + GenesisHash: genesisInitState.GenesisHash, + } + + // create application + approvalProgram := program + clearStateProgram := []byte("\x02") // empty + appCreateFields := transactions.ApplicationCallTxnFields{ + ApprovalProgram: approvalProgram, + ClearStateProgram: clearStateProgram, + GlobalStateSchema: basics.StateSchema{NumByteSlice: 4}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + } + appCreate := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCreateFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCreate, transactions.ApplyData{}) + a.NoError(err) + + appIdx := basics.AppIndex(1) // first tnx => idx = 1 + + // destoy the app + txHeader.Sender = creator + appCallFields := transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.DeleteApplicationOC, + ApplicationID: appIdx, + } + appCall := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + paymentFields := transactions.PaymentTxnFields{ + Amount: basics.MicroAlgos{Raw: 0}, + Receiver: userLocal, + CloseRemainderTo: userLocal, + } + payment := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: txHeader, + PaymentTxnFields: paymentFields, + } + + data := genesisInitState.Accounts[creator] + balance := basics.MicroAlgos{Raw: data.MicroAlgos.Raw - txHeader.Fee.Raw*3} + stx1 := sign(initKeys, appCall) + stx2 := sign(initKeys, payment) + + blk := makeNewEmptyBlock(t, l, genesisID, genesisInitState.Accounts) + txib1, err := blk.EncodeSignedTxn(stx1, transactions.ApplyData{EvalDelta: basics.EvalDelta{ + GlobalDelta: basics.StateDelta{ + "gk": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "global"}, + }}, + }) + a.NoError(err) + txib2, err := blk.EncodeSignedTxn(stx2, transactions.ApplyData{ClosingAmount: balance}) + a.NoError(err) + blk.TxnCounter = blk.TxnCounter + 2 + blk.Payset = append(blk.Payset, txib1, txib2) + blk.TxnRoot, err = blk.PaysetCommit() + a.NoError(err) + err = l.appendUnvalidated(blk) + a.NoError(err) + + // save data into DB and write into local state + l.accts.accountsWriting.Add(1) + l.accts.commitRound(2, 0, 0) + l.reloadLedger() + + // check first write + blk, err = l.Block(1) + a.NoError(err) + a.Nil(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas) + a.Nil(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta) + + // check deletion out + blk, err = l.Block(2) + a.NoError(err) + a.Nil(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta, "gk") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.GlobalDelta["gk"].Bytes, "global") + + pad, err := l.accts.accountsq.lookup(creator) + a.NoError(err) + a.Equal(basics.AccountData{}, pad.accountData) + a.Zero(pad.rowid) +} + +func TestAppAccountDeltaIndicesCompatibility1(t *testing.T) { + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +int 0 +byte "lk0" +byte "local0" +app_local_put +int 1 +byte "lk1" +byte "local1" +app_local_put +success: +int 1 +` + // put into sender account as idx 0, expect 0 + testAppAccountDeltaIndicesCompatibility(t, source, 0) +} + +func TestAppAccountDeltaIndicesCompatibility2(t *testing.T) { + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +int 1 +byte "lk1" +byte "local1" +app_local_put +int 0 +byte "lk0" +byte "local0" +app_local_put +success: +int 1 +` + // put into sender account as idx 1, expect 1 + testAppAccountDeltaIndicesCompatibility(t, source, 1) +} + +func TestAppAccountDeltaIndicesCompatibility3(t *testing.T) { + source := `#pragma version 2 +txn ApplicationID +int 0 +== +bnz success +int 1 +byte "lk" +app_local_get +pop +int 0 +byte "lk0" +byte "local0" +app_local_put +int 1 +byte "lk1" +byte "local1" +app_local_put +success: +int 1 +` + // get sender account as idx 1 but put into sender account as idx 0, expect 1 + testAppAccountDeltaIndicesCompatibility(t, source, 1) +} + +func testAppAccountDeltaIndicesCompatibility(t *testing.T, source string, accountIdx uint64) { + a := require.New(t) + ops, err := logic.AssembleString(source) + a.NoError(err) + a.Greater(len(ops.Program), 1) + program := ops.Program + + // explicitly trigger compatibility mode + proto := config.Consensus[protocol.ConsensusV24] + genesisInitState, initKeys := testGenerateInitState(t, protocol.ConsensusV24) + + creator, err := basics.UnmarshalChecksumAddress("3LN5DBFC2UTPD265LQDP3LMTLGZCQ5M3JV7XTVTGRH5CKSVNQVDFPN6FG4") + a.NoError(err) + userLocal, err := basics.UnmarshalChecksumAddress("UL5C6SRVLOROSB5FGAE6TY34VXPXVR7GNIELUB3DD5KTA4VT6JGOZ6WFAY") + a.NoError(err) + + a.Contains(genesisInitState.Accounts, creator) + a.Contains(genesisInitState.Accounts, userLocal) + + cfg := config.GetDefaultLocal() + l, err := OpenLedger(logging.Base(), t.Name(), true, genesisInitState, cfg) + a.NoError(err) + defer l.Close() + + genesisID := t.Name() + txHeader := transactions.Header{ + Sender: creator, + Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2}, + FirstValid: l.Latest() + 1, + LastValid: l.Latest() + 10, + GenesisID: genesisID, + GenesisHash: genesisInitState.GenesisHash, + } + + // create application + approvalProgram := program + clearStateProgram := []byte("\x02") // empty + appCreateFields := transactions.ApplicationCallTxnFields{ + ApprovalProgram: approvalProgram, + ClearStateProgram: clearStateProgram, + GlobalStateSchema: basics.StateSchema{NumByteSlice: 4}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + } + appCreate := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCreateFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCreate, transactions.ApplyData{}) + a.NoError(err) + + appIdx := basics.AppIndex(1) // first tnx => idx = 1 + + // opt-in + txHeader.Sender = userLocal + appCallFields := transactions.ApplicationCallTxnFields{ + OnCompletion: transactions.OptInOC, + ApplicationID: appIdx, + Accounts: []basics.Address{userLocal}, + } + appCall := transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: txHeader, + ApplicationCallTxnFields: appCallFields, + } + err = l.appendUnvalidatedTx(t, genesisInitState.Accounts, initKeys, appCall, transactions.ApplyData{ + EvalDelta: basics.EvalDelta{ + LocalDeltas: map[uint64]basics.StateDelta{ + accountIdx: { + "lk0": basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "local0", + }, + "lk1": basics.ValueDelta{ + Action: basics.SetBytesAction, + Bytes: "local1"}, + }, + }, + }, + }) + a.NoError(err) + + // save data into DB and write into local state + l.accts.accountsWriting.Add(1) + l.accts.commitRound(2, 0, 0) + l.reloadLedger() + + // check first write + blk, err := l.Block(2) + a.NoError(err) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas, accountIdx) + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx], "lk0") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx]["lk0"].Bytes, "local0") + a.Contains(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx], "lk1") + a.Equal(blk.Payset[0].ApplyData.EvalDelta.LocalDeltas[accountIdx]["lk1"].Bytes, "local1") +} diff --git a/ledger/cow.go b/ledger/cow.go index 09be85c09..ab4e87c51 100644 --- a/ledger/cow.go +++ b/ledger/cow.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" ) // ___________________ @@ -47,7 +48,7 @@ type roundCowParent interface { // and is provided to optimize state schema lookups getStorageLimits(addr basics.Address, aidx basics.AppIndex, global bool) (basics.StateSchema, error) allocated(addr basics.Address, aidx basics.AppIndex, global bool) (bool, error) - getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) + getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) } type roundCowState struct { @@ -61,16 +62,33 @@ type roundCowState struct { // 2. Stateful TEAL evaluation (see SetKey/DelKey) // must be incorporated into mods.accts before passing deltas forward sdeltas map[basics.Address]map[storagePtr]*storageDelta + + // either or not maintain compatibility with original app refactoring behavior + // this is needed for generating old eval delta in new code + compatibilityMode bool + // cache mainaining accountIdx used in getKey for local keys access + compatibilityGetKeyCache map[basics.Address]map[storagePtr]uint64 } func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader, prevTimestamp int64, hint int) *roundCowState { - return &roundCowState{ + cb := roundCowState{ lookupParent: b, commitParent: nil, proto: config.Consensus[hdr.CurrentProtocol], mods: ledgercore.MakeStateDelta(&hdr, prevTimestamp, hint), sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), } + + // compatibilityMode retains producing application' eval deltas under the following rule: + // local delta has account index as it specified in TEAL either in set/del key or prior get key calls. + // The predicate is that complex in order to cover all the block seen on testnet and mainnet. + compatibilityMode := (hdr.CurrentProtocol == protocol.ConsensusV24) && + (hdr.NextProtocol != protocol.ConsensusV26 || (hdr.UpgradePropose == "" && hdr.UpgradeApprove == false && hdr.Round < hdr.UpgradeState.NextProtocolVoteBefore)) + if compatibilityMode { + cb.compatibilityMode = true + cb.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) + } + return &cb } func (cb *roundCowState) deltas() ledgercore.StateDelta { @@ -196,13 +214,19 @@ func (cb *roundCowState) setCompactCertNext(rnd basics.Round) { } func (cb *roundCowState) child(hint int) *roundCowState { - return &roundCowState{ + ch := roundCowState{ lookupParent: cb, commitParent: cb, proto: cb.proto, mods: ledgercore.MakeStateDelta(cb.mods.Hdr, cb.mods.PrevTimestamp, hint), sdeltas: make(map[basics.Address]map[storagePtr]*storageDelta), } + + if cb.compatibilityMode { + ch.compatibilityMode = cb.compatibilityMode + ch.compatibilityGetKeyCache = make(map[basics.Address]map[storagePtr]uint64) + } + return &ch } func (cb *roundCowState) commitToParent() { diff --git a/ledger/cow_test.go b/ledger/cow_test.go index 307f4f358..a4df0f1c1 100644 --- a/ledger/cow_test.go +++ b/ledger/cow_test.go @@ -63,7 +63,7 @@ func (ml *mockLedger) allocated(addr basics.Address, aidx basics.AppIndex, globa return true, nil } -func (ml *mockLedger) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { +func (ml *mockLedger) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { return basics.TealValue{}, false, nil } diff --git a/ledger/eval.go b/ledger/eval.go index 1da34bf81..eb9596c9d 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -137,7 +137,7 @@ func (x *roundCowBase) allocated(addr basics.Address, aidx basics.AppIndex, glob // getKey gets the value for a particular key in some storage // associated with an application globally or locally -func (x *roundCowBase) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string) (basics.TealValue, bool, error) { +func (x *roundCowBase) getKey(addr basics.Address, aidx basics.AppIndex, global bool, key string, accountIdx uint64) (basics.TealValue, bool, error) { ad, _, err := x.l.LookupWithoutRewards(x.rnd, addr) if err != nil { return basics.TealValue{}, false, err diff --git a/protocol/consensus.go b/protocol/consensus.go index 7b2a50793..02e6e1e26 100644 --- a/protocol/consensus.go +++ b/protocol/consensus.go @@ -143,6 +143,11 @@ const ConsensusV26 = ConsensusVersion( "https://github.com/algorandfoundation/specs/tree/ac2255d586c4474d4ebcf3809acccb59b7ef34ff", ) +// ConsensusV27 updates ApplyDelta.EvalDelta.LocalDeltas format +const ConsensusV27 = ConsensusVersion( + "https://github.com/algorandfoundation/specs/tree/d050b3cade6d5c664df8bd729bf219f179812595", +) + // ConsensusFuture is a protocol that should not appear in any production // network, but is used to test features before they are released. const ConsensusFuture = ConsensusVersion( @@ -155,7 +160,7 @@ const ConsensusFuture = ConsensusVersion( // ConsensusCurrentVersion is the latest version and should be used // when a specific version is not provided. -const ConsensusCurrentVersion = ConsensusV26 +const ConsensusCurrentVersion = ConsensusV27 // Error is used to indicate that an unsupported protocol has been detected. type Error ConsensusVersion |