summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Lee <64482439+algojohnlee@users.noreply.github.com>2021-04-28 12:32:28 -0400
committerGitHub <noreply@github.com>2021-04-28 12:32:28 -0400
commit219b78d98184274b4ddccff47cdd1fa8091c8dac (patch)
tree07faa9c96a48b5359eaffe3a5e9916aaef91af5b
parent304815d00b9512cf9f91dbb987fead35894676f4 (diff)
parentfcfaf87fc6c219d891219f190788b2f6cc3066ed (diff)
Merge pull request #2110 from onetechnical/onetechnical/relstable2.5.6v2.5.6-stable
go-algorand 2.5.6-stable
-rw-r--r--buildnumber.dat2
-rw-r--r--config/consensus.go22
-rw-r--r--crypto/merklearray/worker.go10
-rw-r--r--data/transactions/logic/eval.go12
-rw-r--r--data/transactions/logic/evalStateful_test.go6
-rw-r--r--ledger/accountdb.go46
-rw-r--r--ledger/acctupdates.go63
-rw-r--r--ledger/appcow.go98
-rw-r--r--ledger/appcow_test.go262
-rw-r--r--ledger/applications.go24
-rw-r--r--ledger/applications_test.go732
-rw-r--r--ledger/cow.go30
-rw-r--r--ledger/cow_test.go2
-rw-r--r--ledger/eval.go2
-rw-r--r--protocol/consensus.go7
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