diff options
author | John Lee <64482439+algojohnlee@users.noreply.github.com> | 2021-08-10 09:08:32 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-10 09:08:32 -0400 |
commit | 7b53f6f029147fa4abd74739313a01aa7ee41715 (patch) | |
tree | bfc9315fa1361855e4746b86aeb5e12f56359af4 | |
parent | 1ad773e0f9a11592e7ffdce749ed585183938105 (diff) | |
parent | 8ce1e6c69e5c4cf080fb170625e2551a387e9e63 (diff) |
Merge pull request #2704 from algorand/hotfix/v2.9.1v2.9.1-stable
Hotfix/v2.9.1
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | buildnumber.dat | 2 | ||||
-rw-r--r-- | config/consensus.go | 16 | ||||
-rw-r--r-- | config/version.go | 2 | ||||
-rw-r--r-- | daemon/algod/api/client/restClient.go | 8 | ||||
-rw-r--r-- | data/basics/overflow.go | 32 | ||||
-rw-r--r-- | data/basics/units_test.go | 18 | ||||
-rw-r--r-- | data/transactions/transaction.go | 8 | ||||
-rw-r--r-- | data/transactions/transaction_test.go | 49 | ||||
-rw-r--r-- | ledger/appcow.go | 25 | ||||
-rw-r--r-- | ledger/appcow_test.go | 4 | ||||
-rw-r--r-- | ledger/apply/application.go | 60 | ||||
-rw-r--r-- | ledger/apply/application_test.go | 275 | ||||
-rw-r--r-- | ledger/apply/apply.go | 17 | ||||
-rw-r--r-- | ledger/apply/asset.go | 21 | ||||
-rw-r--r-- | ledger/apply/keyreg_test.go | 16 | ||||
-rw-r--r-- | ledger/apply/mockBalances_test.go | 14 | ||||
-rw-r--r-- | ledger/assetcow.go | 48 | ||||
-rw-r--r-- | ledger/cow.go | 20 | ||||
-rw-r--r-- | ledger/cow_test.go | 2 | ||||
-rw-r--r-- | ledger/eval.go | 15 | ||||
-rw-r--r-- | libgoal/libgoal.go | 10 | ||||
-rw-r--r-- | protocol/consensus.go | 7 | ||||
-rwxr-xr-x | scripts/travis/run_tests.sh | 4 | ||||
-rw-r--r-- | test/e2e-go/features/transactions/app_pages_test.go | 183 | ||||
-rwxr-xr-x | test/scripts/e2e_subs/e2e-app-extra-pages.sh | 35 |
26 files changed, 713 insertions, 182 deletions
diff --git a/.travis.yml b/.travis.yml index c71c66279..c4212d488 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,11 @@ if: tag IS blank stages: - name: build_commit - if: NOT (branch =~ /^rel\//) AND type != pull_request + if: NOT (branch =~ /^hotfix\//) AND NOT (branch =~ /^rel\//) AND type != pull_request - name: build_pr if: type = pull_request - name: build_release - if: branch =~ /^rel\// AND type != pull_request + if: (branch =~ /^hotfix\// OR branch =~ /^rel\//) AND type != pull_request - name: deploy if: branch =~ /^rel\// AND type != pull_request - name: post_deploy diff --git a/buildnumber.dat b/buildnumber.dat index 573541ac9..d00491fd7 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -0 +1 diff --git a/config/consensus.go b/config/consensus.go index 5bee45ac6..b6954231b 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -381,6 +381,8 @@ type ConsensusParams struct { // 5. checking that in the case of going online the VoteFirst is less or equal to the LastValid+1. // 6. checking that in the case of going online the VoteFirst is less or equal to the next network round. EnableKeyregCoherencyCheck bool + + EnableExtraPagesOnAppUpdate bool } // PaysetCommitType enumerates possible ways for the block header to commit to @@ -969,9 +971,21 @@ func initConsensusProtocols() { // v27 can be upgraded to v28, with an update delay of 7 days ( see calculation above ) v27.ApprovedUpgrades[protocol.ConsensusV28] = 140000 + // v29 fixes application update by using ExtraProgramPages in size calculations + v29 := v28 + v29.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + + // Enable ExtraProgramPages for application update + v29.EnableExtraPagesOnAppUpdate = true + + Consensus[protocol.ConsensusV29] = v29 + + // v28 can be upgraded to v29, with an update delay of 3 days ( see calculation above ) + v28.ApprovedUpgrades[protocol.ConsensusV29] = 60000 + // ConsensusFuture is used to test features that are implemented // but not yet released in a production protocol version. - vFuture := v28 + vFuture := v29 vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} // FilterTimeout for period 0 should take a new optimized, configured value, need to revisit this later diff --git a/config/version.go b/config/version.go index 6b19636e1..b2d9ca833 100644 --- a/config/version.go +++ b/config/version.go @@ -33,7 +33,7 @@ const VersionMajor = 2 // VersionMinor is the Minor semantic version number (x.#.z) - changed when backwards-compatible features are introduced. // Not enforced until after initial public release (x > 0). -const VersionMinor = 8 +const VersionMinor = 9 // Version is the type holding our full version information. type Version struct { diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index b231e80a7..a00f25880 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -434,6 +434,14 @@ func (client RestClient) PendingTransactionInformation(transactionID string) (re return } +// PendingTransactionInformationV2 gets information about a recently issued +// transaction. See PendingTransactionInformation for more details. +func (client RestClient) PendingTransactionInformationV2(transactionID string) (response generatedV2.PendingTransactionResponse, err error) { + transactionID = stripTransaction(transactionID) + err = client.get(&response, fmt.Sprintf("/v2/transactions/pending/%s", transactionID), nil) + return +} + // SuggestedFee gets the recommended transaction fee from the node func (client RestClient) SuggestedFee() (response v1.TransactionFee, err error) { err = client.get(&response, "/v1/transactions/fee", nil) diff --git a/data/basics/overflow.go b/data/basics/overflow.go index 7e6ac9511..7a2a3df8b 100644 --- a/data/basics/overflow.go +++ b/data/basics/overflow.go @@ -33,6 +33,13 @@ func OAdd16(a uint16, b uint16) (res uint16, overflowed bool) { return } +// OAdd32 adds 2 uint32 values with overflow detection +func OAdd32(a uint32, b uint32) (res uint32, overflowed bool) { + res = a + b + overflowed = res < a + return +} + // OAdd adds 2 values with overflow detection func OAdd(a uint64, b uint64) (res uint64, overflowed bool) { res = a + b @@ -47,6 +54,13 @@ func OSub(a uint64, b uint64) (res uint64, overflowed bool) { return } +// OSub32 subtracts b from a with overflow detection +func OSub32(a uint32, b uint32) (res uint32, overflowed bool) { + res = a - b + overflowed = res > a + return +} + // OMul multiplies 2 values with overflow detection func OMul(a uint64, b uint64) (res uint64, overflowed bool) { if b == 0 { @@ -78,6 +92,15 @@ func AddSaturate(a uint64, b uint64) uint64 { return res } +// AddSaturate32 adds 2 uint32 values with saturation on overflow +func AddSaturate32(a uint32, b uint32) uint32 { + res, overflowed := OAdd32(a, b) + if overflowed { + return math.MaxUint32 + } + return res +} + // SubSaturate subtracts 2 values with saturation on underflow func SubSaturate(a uint64, b uint64) uint64 { res, overflowed := OSub(a, b) @@ -87,6 +110,15 @@ func SubSaturate(a uint64, b uint64) uint64 { return res } +// SubSaturate32 subtracts 2 uint32 values with saturation on underflow +func SubSaturate32(a uint32, b uint32) uint32 { + res, overflowed := OSub32(a, b) + if overflowed { + return 0 + } + return res +} + // Add16 adds 2 uint16 values with overflow detection func (t *OverflowTracker) Add16(a uint16, b uint16) uint16 { res, overflowed := OAdd16(a, b) diff --git a/data/basics/units_test.go b/data/basics/units_test.go index 74e69c82e..b0b3a8885 100644 --- a/data/basics/units_test.go +++ b/data/basics/units_test.go @@ -17,6 +17,7 @@ package basics import ( + "math" "testing" "github.com/stretchr/testify/require" @@ -30,6 +31,23 @@ func TestSubSaturate(t *testing.T) { require.Equal(t, b.SubSaturate(a), Round(1)) } +func TestSubSaturate32(t *testing.T) { + require.Equal(t, uint32(0), SubSaturate32(0, 1)) + require.Equal(t, uint32(0), SubSaturate32(1, 2)) + require.Equal(t, uint32(0), SubSaturate32(1, 1)) + require.Equal(t, uint32(0), SubSaturate32(1, math.MaxUint32)) + require.Equal(t, uint32(1), SubSaturate32(2, 1)) + require.Equal(t, uint32(math.MaxUint32-1), SubSaturate32(math.MaxUint32, 1)) +} + +func TestAddSaturate32(t *testing.T) { + require.Equal(t, uint32(1), AddSaturate32(0, 1)) + require.Equal(t, uint32(math.MaxUint32-1), AddSaturate32(math.MaxUint32-2, 1)) + require.Equal(t, uint32(math.MaxUint32), AddSaturate32(math.MaxUint32, 0)) + require.Equal(t, uint32(math.MaxUint32), AddSaturate32(math.MaxUint32-1, 1)) + require.Equal(t, uint32(math.MaxUint32), AddSaturate32(math.MaxUint32, 2)) +} + func TestRoundUpToMultipleOf(t *testing.T) { r := Round(24) for n := Round(1); n < Round(100); n++ { diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index c0e3e863f..552255796 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -344,6 +344,7 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa } } + effectiveEPP := tx.ExtraProgramPages // Schemas and ExtraProgramPages may only be set during application creation if tx.ApplicationID != 0 { if tx.LocalStateSchema != (basics.StateSchema{}) || @@ -353,6 +354,11 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa if tx.ExtraProgramPages != 0 { return fmt.Errorf("tx.ExtraProgramPages is immutable") } + + if proto.EnableExtraPagesOnAppUpdate { + effectiveEPP = uint32(proto.MaxExtraAppProgramPages) + } + } // Limit total number of arguments @@ -396,7 +402,7 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa lap := len(tx.ApprovalProgram) lcs := len(tx.ClearStateProgram) - pages := int(1 + tx.ExtraProgramPages) + pages := int(1 + effectiveEPP) if lap > pages*proto.MaxAppProgramLen { return fmt.Errorf("approval program too long. max len %d bytes", pages*proto.MaxAppProgramLen) } diff --git a/data/transactions/transaction_test.go b/data/transactions/transaction_test.go index 5e8151467..26631dee4 100644 --- a/data/transactions/transaction_test.go +++ b/data/transactions/transaction_test.go @@ -228,6 +228,7 @@ func TestWellFormedErrors(t *testing.T) { curProto := config.Consensus[protocol.ConsensusCurrentVersion] futureProto := config.Consensus[protocol.ConsensusFuture] protoV27 := config.Consensus[protocol.ConsensusV27] + protoV28 := config.Consensus[protocol.ConsensusV28] addr1, err := basics.UnmarshalChecksumAddress("NDQCJNNY5WWWFLP4GFZ7MEF2QJSMZYK6OWIV2AQ7OMAVLEFCGGRHFPKJJA") require.NoError(t, err) okHeader := Header{ @@ -447,6 +448,54 @@ func TestWellFormedErrors(t *testing.T) { proto: futureProto, expectedError: fmt.Errorf("tx has too many references, max is 8"), }, + { + tx: Transaction{ + Type: protocol.ApplicationCallTx, + Header: okHeader, + ApplicationCallTxnFields: ApplicationCallTxnFields{ + ApplicationID: 1, + ApprovalProgram: []byte(strings.Repeat("X", 1025)), + ClearStateProgram: []byte(strings.Repeat("X", 1025)), + ExtraProgramPages: 0, + OnCompletion: UpdateApplicationOC, + }, + }, + spec: specialAddr, + proto: protoV28, + expectedError: fmt.Errorf("app programs too long. max total len %d bytes", curProto.MaxAppProgramLen), + }, + { + tx: Transaction{ + Type: protocol.ApplicationCallTx, + Header: okHeader, + ApplicationCallTxnFields: ApplicationCallTxnFields{ + ApplicationID: 1, + ApprovalProgram: []byte(strings.Repeat("X", 1025)), + ClearStateProgram: []byte(strings.Repeat("X", 1025)), + ExtraProgramPages: 0, + OnCompletion: UpdateApplicationOC, + }, + }, + spec: specialAddr, + proto: futureProto, + }, + { + tx: Transaction{ + Type: protocol.ApplicationCallTx, + Header: okHeader, + ApplicationCallTxnFields: ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("write"), + }, + ExtraProgramPages: 1, + OnCompletion: UpdateApplicationOC, + }, + }, + spec: specialAddr, + proto: protoV28, + expectedError: fmt.Errorf("tx.ExtraProgramPages is immutable"), + }, } for _, usecase := range usecases { err := usecase.tx.WellFormed(usecase.spec, usecase.proto) diff --git a/ledger/appcow.go b/ledger/appcow.go index eafa0930f..4b41c92a5 100644 --- a/ledger/appcow.go +++ b/ledger/appcow.go @@ -222,7 +222,7 @@ func errAlreadyStorage(addr basics.Address, aidx basics.AppIndex, global bool) e } // Allocate creates kv storage for a given {addr, aidx, global}. It is called on app creation (global) or opting in (local) -func (cb *roundCowState) Allocate(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error { +func (cb *roundCowState) AllocateApp(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error { // Check that account is not already opted in allocated, err := cb.allocated(addr, aidx, global) if err != nil { @@ -241,11 +241,21 @@ func (cb *roundCowState) Allocate(addr basics.Address, aidx basics.AppIndex, glo lsd.action = allocAction lsd.maxCounts = &space + if global { + cb.mods.Creatables[basics.CreatableIndex(aidx)] = ledgercore.ModifiedCreatable{ + Ctype: basics.AppCreatable, + Creator: addr, + Created: true, + } + } + + cb.trackCreatable(basics.CreatableIndex(aidx)) + return nil } // Deallocate clears storage for {addr, aidx, global}. It happens on app deletion (global) or closing out (local) -func (cb *roundCowState) Deallocate(addr basics.Address, aidx basics.AppIndex, global bool) error { +func (cb *roundCowState) DeallocateApp(addr basics.Address, aidx basics.AppIndex, global bool) error { // Check that account has allocated storage allocated, err := cb.allocated(addr, aidx, global) if err != nil { @@ -265,6 +275,15 @@ func (cb *roundCowState) Deallocate(addr basics.Address, aidx basics.AppIndex, g lsd.counts = &basics.StateSchema{} lsd.maxCounts = &basics.StateSchema{} lsd.kvCow = make(stateDelta) + + if global { + cb.mods.Creatables[basics.CreatableIndex(aidx)] = ledgercore.ModifiedCreatable{ + Ctype: basics.AppCreatable, + Creator: addr, + Created: false, + } + } + return nil } @@ -619,7 +638,7 @@ 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 PutWithCreatable + // at least preceded by a call to Put() params, ok := owned[aapp.aidx] if !ok { return basics.AccountData{}, fmt.Errorf("could not find existing params for %v", aapp.aidx) diff --git a/ledger/appcow_test.go b/ledger/appcow_test.go index 75dbfac91..0994ed2f6 100644 --- a/ledger/appcow_test.go +++ b/ledger/appcow_test.go @@ -239,7 +239,7 @@ func TestCowStorage(t *testing.T) { NumUint: rand.Uint64(), NumByteSlice: rand.Uint64(), } - err := cow.Allocate(addr, sptr.aidx, sptr.global, rschema) + err := cow.AllocateApp(addr, sptr.aidx, sptr.global, rschema) if actuallyAllocated { require.Error(t, err) require.Contains(t, err.Error(), "cannot allocate") @@ -253,7 +253,7 @@ func TestCowStorage(t *testing.T) { // Deallocate if rand.Float32() < 0.25 { actuallyAllocated := st.allocated(aapp) - err := cow.Deallocate(addr, sptr.aidx, sptr.global) + err := cow.DeallocateApp(addr, sptr.aidx, sptr.global) if actuallyAllocated { require.NoError(t, err) err := st.dealloc(aapp) diff --git a/ledger/apply/application.go b/ledger/apply/application.go index dc9da3122..644c8e396 100644 --- a/ledger/apply/application.go +++ b/ledger/apply/application.go @@ -118,24 +118,17 @@ func createApplication(ac *transactions.ApplicationCallTxnFields, balances Balan // Update the cached TotalExtraAppPages for this account, used // when computing MinBalance totalExtraPages := record.TotalExtraAppPages - totalExtraPages += ac.ExtraProgramPages + totalExtraPages = basics.AddSaturate32(totalExtraPages, ac.ExtraProgramPages) record.TotalExtraAppPages = totalExtraPages - // Tell the cow what app we created - created := &basics.CreatableLocator{ - Creator: creator, - Type: basics.AppCreatable, - Index: basics.CreatableIndex(appIdx), - } - // Write back to the creator's balance record - err = balances.PutWithCreatable(creator, record, created, nil) + err = balances.Put(creator, record) if err != nil { return 0, err } // Allocate global storage - err = balances.Allocate(creator, appIdx, true, ac.GlobalStateSchema) + err = balances.AllocateApp(creator, appIdx, true, ac.GlobalStateSchema) if err != nil { return 0, err } @@ -150,6 +143,8 @@ func deleteApplication(balances Balances, creator basics.Address, appIdx basics. return err } + record.AppParams = cloneAppParams(record.AppParams) + // Update the TotalAppSchema used for MinBalance calculation, // since the creator no longer has to store the GlobalState totalSchema := record.TotalAppSchema @@ -157,31 +152,27 @@ func deleteApplication(balances Balances, creator basics.Address, appIdx basics. totalSchema = totalSchema.SubSchema(globalSchema) record.TotalAppSchema = totalSchema - // Delete the AppParams - record.AppParams = cloneAppParams(record.AppParams) - delete(record.AppParams, appIdx) - // Delete app's extra program pages totalExtraPages := record.TotalExtraAppPages if totalExtraPages > 0 { - extraPages := record.AppParams[appIdx].ExtraProgramPages - totalExtraPages -= extraPages + proto := balances.ConsensusParams() + if proto.EnableExtraPagesOnAppUpdate { + extraPages := record.AppParams[appIdx].ExtraProgramPages + totalExtraPages = basics.SubSaturate32(totalExtraPages, extraPages) + } record.TotalExtraAppPages = totalExtraPages } - // Tell the cow what app we deleted - deleted := &basics.CreatableLocator{ - Creator: creator, - Type: basics.AppCreatable, - Index: basics.CreatableIndex(appIdx), - } - err = balances.PutWithCreatable(creator, record, nil, deleted) + // Delete the AppParams + delete(record.AppParams, appIdx) + + err = balances.Put(creator, record) if err != nil { return err } // Deallocate global storage - err = balances.Deallocate(creator, appIdx, true) + err = balances.DeallocateApp(creator, appIdx, true) if err != nil { return err } @@ -199,6 +190,23 @@ func updateApplication(ac *transactions.ApplicationCallTxnFields, balances Balan // Fill in the new programs record.AppParams = cloneAppParams(record.AppParams) params := record.AppParams[appIdx] + proto := balances.ConsensusParams() + // when proto.EnableExtraPageOnAppUpdate is false, WellFormed rejects all updates with a multiple-page program + if proto.EnableExtraPagesOnAppUpdate { + lap := len(ac.ApprovalProgram) + lcs := len(ac.ClearStateProgram) + pages := int(1 + params.ExtraProgramPages) + if lap > pages*proto.MaxAppProgramLen { + return fmt.Errorf("updateApplication approval program too long. max len %d bytes", pages*proto.MaxAppProgramLen) + } + if lcs > pages*proto.MaxAppProgramLen { + return fmt.Errorf("updateApplication clear state program too long. max len %d bytes", pages*proto.MaxAppProgramLen) + } + if lap+lcs > pages*proto.MaxAppTotalProgramLen { + return fmt.Errorf("updateApplication app programs too long, %d. max total len %d bytes", lap+lcs, pages*proto.MaxAppTotalProgramLen) + } + } + params.ApprovalProgram = ac.ApprovalProgram params.ClearStateProgram = ac.ClearStateProgram @@ -243,7 +251,7 @@ func optInApplication(balances Balances, sender basics.Address, appIdx basics.Ap } // Allocate local storage - err = balances.Allocate(sender, appIdx, false, params.LocalStateSchema) + err = balances.AllocateApp(sender, appIdx, false, params.LocalStateSchema) if err != nil { return err } @@ -281,7 +289,7 @@ func closeOutApplication(balances Balances, sender basics.Address, appIdx basics } // Deallocate local storage - err = balances.Deallocate(sender, appIdx, false) + err = balances.DeallocateApp(sender, appIdx, false) if err != nil { return err } diff --git a/ledger/apply/application_test.go b/ledger/apply/application_test.go index 0633db8d1..8b320b6e2 100644 --- a/ledger/apply/application_test.go +++ b/ledger/apply/application_test.go @@ -103,11 +103,9 @@ type testBalances struct { proto config.ConsensusParams put int // Put calls counter - putWith int // PutWithCreatable calls counter putBalances map[basics.Address]basics.AccountData - putWithBalances map[basics.Address]basics.AccountData - putWithNew []basics.CreatableLocator - putWithDel []basics.CreatableLocator + createdCreatables []basics.CreatableLocator + deletedCreatables []basics.CreatableLocator allocatedAppIdx basics.AppIndex deAllocatedAppIdx basics.AppIndex @@ -141,21 +139,6 @@ func (b *testBalances) Put(addr basics.Address, ad basics.AccountData) error { return nil } -func (b *testBalances) PutWithCreatable(addr basics.Address, ad basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { - b.putWith++ - if b.putWithBalances == nil { - b.putWithBalances = make(map[basics.Address]basics.AccountData) - } - b.putWithBalances[addr] = ad - if newCreatable != nil { - b.putWithNew = append(b.putWithNew, *newCreatable) - } - if deletedCreatable != nil { - b.putWithDel = append(b.putWithDel, *deletedCreatable) - } - return nil -} - func (b *testBalances) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { if ctype == basics.AppCreatable { aidx := basics.AppIndex(cidx) @@ -176,13 +159,60 @@ func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, s func (b *testBalances) ConsensusParams() config.ConsensusParams { return b.proto } -func (b *testBalances) Allocate(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error { + +func (b *testBalances) AllocateApp(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error { b.allocatedAppIdx = aidx + + if global { + locator := basics.CreatableLocator{ + Type: basics.AppCreatable, + Creator: addr, + Index: basics.CreatableIndex(aidx), + } + b.createdCreatables = append(b.createdCreatables, locator) + } + return nil } -func (b *testBalances) Deallocate(addr basics.Address, aidx basics.AppIndex, global bool) error { +func (b *testBalances) DeallocateApp(addr basics.Address, aidx basics.AppIndex, global bool) error { b.deAllocatedAppIdx = aidx + + if global { + locator := basics.CreatableLocator{ + Type: basics.AppCreatable, + Creator: addr, + Index: basics.CreatableIndex(aidx), + } + b.deletedCreatables = append(b.deletedCreatables, locator) + } + + return nil +} + +func (b *testBalances) AllocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { + if global { + locator := basics.CreatableLocator{ + Type: basics.AppCreatable, + Creator: addr, + Index: basics.CreatableIndex(index), + } + b.createdCreatables = append(b.createdCreatables, locator) + } + + return nil +} + +func (b *testBalances) DeallocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { + if global { + locator := basics.CreatableLocator{ + Type: basics.AppCreatable, + Creator: addr, + Index: basics.CreatableIndex(index), + } + b.deletedCreatables = append(b.deletedCreatables, locator) + } + return nil } @@ -206,14 +236,6 @@ func (b *testBalancesPass) Put(addr basics.Address, ad basics.AccountData) error return nil } -func (b *testBalancesPass) PutWithCreatable(addr basics.Address, ad basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { - if b.balances == nil { - b.balances = make(map[basics.Address]basics.AccountData) - } - b.balances[addr] = ad - return nil -} - func (b *testBalancesPass) ConsensusParams() config.ConsensusParams { return b.proto } @@ -230,14 +252,12 @@ func (b *testBalancesPass) StatefulEval(params logic.EvalParams, aidx basics.App return true, b.delta, nil } -// ResetWrites clears side effects of Put/PutWithCreatable +// ResetWrites clears side effects of Put. func (b *testBalances) ResetWrites() { b.put = 0 - b.putWith = 0 b.putBalances = nil - b.putWithBalances = nil - b.putWithNew = []basics.CreatableLocator{} - b.putWithDel = []basics.CreatableLocator{} + b.createdCreatables = []basics.CreatableLocator{} + b.deletedCreatables = []basics.CreatableLocator{} b.allocatedAppIdx = 0 } @@ -245,6 +265,10 @@ func (b *testBalances) SetProto(name protocol.ConsensusVersion) { b.proto = config.Consensus[name] } +func (b *testBalances) SetParams(params config.ConsensusParams) { + b.proto = params +} + type testEvaluator struct { pass bool delta basics.EvalDelta @@ -411,11 +435,8 @@ func TestAppCallCreate(t *testing.T) { appIdx, err = createApplication(&ac, &b, creator, txnCounter) a.NoError(err) a.Equal(txnCounter+1, uint64(appIdx)) - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) nbr, ok := b.putBalances[creator] - a.False(ok) - nbr, ok = b.putWithBalances[creator] a.True(ok) params, ok := nbr.AppParams[appIdx] a.True(ok) @@ -423,7 +444,7 @@ func TestAppCallCreate(t *testing.T) { a.Equal(ac.ClearStateProgram, params.ClearStateProgram) a.Equal(ac.LocalStateSchema, params.LocalStateSchema) a.Equal(ac.GlobalStateSchema, params.GlobalStateSchema) - a.True(len(b.putWithNew) > 0) + a.Equal(1, len(b.createdCreatables)) } // TestAppCallApplyCreate carefully tracks and validates balance record updates @@ -448,7 +469,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "ApplicationCall cannot have nil ApplyData") a.Equal(0, b.put) - a.Equal(0, b.putWith) b.balances = make(map[basics.Address]basics.AccountData) b.balances[creator] = basics.AccountData{} @@ -458,7 +478,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "max created apps per acct is 0") a.Equal(0, b.put) - a.Equal(0, b.putWith) b.SetProto(protocol.ConsensusFuture) proto := b.ConsensusParams() @@ -470,14 +489,13 @@ func TestAppCallApplyCreate(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.Error(err) a.Contains(err.Error(), "applications that do not exist") - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) appIdx := basics.AppIndex(txnCounter + 1) b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} // save the created app info to the side - saved := b.putWithBalances[creator] + saved := b.putBalances[creator] b.ResetWrites() @@ -486,8 +504,7 @@ func TestAppCallApplyCreate(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.Error(err) a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", appIdx)) - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) b.ResetWrites() @@ -500,15 +517,14 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "transaction rejected by ApprovalProgram") a.Equal(uint64(b.allocatedAppIdx), txnCounter+1) - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) // ensure original balance record in the mock was not changed // this ensure proper cloning and any in-intended in-memory modifications // // known artefact of cloning AppLocalState even with empty update, nil map vs empty map saved.AppLocalStates = map[basics.AppIndex]basics.AppLocalState{} a.Equal(saved, b.balances[creator]) - saved = b.putWithBalances[creator] + saved = b.putBalances[creator] b.ResetWrites() @@ -523,10 +539,9 @@ func TestAppCallApplyCreate(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(appIdx, b.allocatedAppIdx) - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) a.Equal(saved, b.balances[creator]) - br := b.putWithBalances[creator] + br := b.putBalances[creator] a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) a.Equal([]byte{1}, br.AppParams[appIdx].ClearStateProgram) a.Equal(basics.TealKeyValue(nil), br.AppParams[appIdx].GlobalState) @@ -538,7 +553,7 @@ func TestAppCallApplyCreate(t *testing.T) { ac.ExtraProgramPages = 1 err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) - br = b.putWithBalances[creator] + br = b.putBalances[creator] a.Equal(uint32(1), br.AppParams[appIdx].ExtraProgramPages) a.Equal(uint32(1), br.TotalExtraAppPages) } @@ -606,13 +621,11 @@ func TestAppCallOptIn(t *testing.T) { a.Error(err) a.Contains(err.Error(), "cannot opt in app") a.Equal(0, b.put) - a.Equal(0, b.putWith) b.SetProto(protocol.ConsensusFuture) err = optInApplication(&b, sender, appIdx, params) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br := b.putBalances[sender] a.Equal(basics.AccountData{AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}}, br) @@ -625,7 +638,6 @@ func TestAppCallOptIn(t *testing.T) { a.Error(err) a.Contains(err.Error(), "has already opted in to app") a.Equal(0, b.put) - a.Equal(0, b.putWith) b.ResetWrites() @@ -635,7 +647,6 @@ func TestAppCallOptIn(t *testing.T) { err = optInApplication(&b, sender, appIdx, params) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) b.ResetWrites() @@ -648,7 +659,6 @@ func TestAppCallOptIn(t *testing.T) { err = optInApplication(&b, sender, appIdx, params) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.putBalances[sender] a.Equal( basics.AccountData{ @@ -707,7 +717,6 @@ func TestAppCallClearState(t *testing.T) { a.Error(err) a.Contains(err.Error(), "is not currently opted in to app") a.Equal(0, b.put) - a.Equal(0, b.putWith) // check non-existing app with empty opt-in b.balances[sender] = basics.AccountData{ @@ -716,7 +725,6 @@ func TestAppCallClearState(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br := b.putBalances[sender] a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) @@ -735,7 +743,6 @@ func TestAppCallClearState(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.putBalances[sender] a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) @@ -762,7 +769,6 @@ func TestAppCallClearState(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.putBalances[sender] a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) @@ -778,7 +784,6 @@ func TestAppCallClearState(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.putBalances[sender] a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) @@ -809,7 +814,6 @@ func TestAppCallClearState(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) a.Equal(appIdx, b.deAllocatedAppIdx) a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) @@ -860,7 +864,6 @@ func TestAppCallApplyCloseOut(t *testing.T) { a.Error(err) a.Contains(err.Error(), "transaction rejected by ApprovalProgram") a.Equal(0, b.put) - a.Equal(0, b.putWith) br := b.balances[creator] a.Equal(cbr, br) a.Equal(basics.EvalDelta{}, ad.EvalDelta) @@ -872,7 +875,6 @@ func TestAppCallApplyCloseOut(t *testing.T) { a.Error(err) a.Contains(err.Error(), "is not opted in to app") a.Equal(0, b.put) - a.Equal(0, b.putWith) br = b.balances[creator] a.Equal(cbr, br) a.Equal(basics.EvalDelta{}, ad.EvalDelta) @@ -888,7 +890,6 @@ func TestAppCallApplyCloseOut(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.putBalances[creator] a.NotEqual(cbr, br) a.Equal(basics.TealKeyValue(nil), br.AppParams[appIdx].GlobalState) @@ -896,6 +897,7 @@ func TestAppCallApplyCloseOut(t *testing.T) { a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) a.Equal(basics.StateSchema{NumUint: 0}, br.TotalAppSchema) + } func TestAppCallApplyUpdate(t *testing.T) { @@ -935,7 +937,7 @@ func TestAppCallApplyUpdate(t *testing.T) { b.balances[creator] = cp b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} - b.SetProto(protocol.ConsensusFuture) + b.SetProto(protocol.ConsensusV28) proto := b.ConsensusParams() ep.Proto = &proto @@ -944,7 +946,6 @@ func TestAppCallApplyUpdate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "transaction rejected by ApprovalProgram") a.Equal(0, b.put) - a.Equal(0, b.putWith) br := b.balances[creator] a.Equal(cbr, br) a.Equal(basics.EvalDelta{}, ad.EvalDelta) @@ -955,13 +956,90 @@ func TestAppCallApplyUpdate(t *testing.T) { err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(1, b.put) - a.Equal(0, b.putWith) br = b.balances[creator] a.Equal(cbr, br) br = b.putBalances[creator] a.Equal([]byte{2}, br.AppParams[appIdx].ApprovalProgram) a.Equal([]byte{2}, br.AppParams[appIdx].ClearStateProgram) a.Equal(basics.EvalDelta{}, ad.EvalDelta) + + //check program len check happens in future consensus proto version + b.SetProto(protocol.ConsensusFuture) + proto = b.ConsensusParams() + ep.Proto = &proto + + // check app program len + params = basics.AppParams{ + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + ExtraProgramPages: 1, + } + h = transactions.Header{ + Sender: sender, + } + + b.balances = make(map[basics.Address]basics.AccountData) + cbr = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + } + cp = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + } + b.balances[creator] = cp + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + // check extraProgramPages is used + appr := make([]byte, 2*proto.MaxAppProgramLen+1) + appr[0] = 4 // version 4 + + var tests = []struct { + name string + approval []byte + clear []byte + }{ + {"approval", appr, []byte{2}}, + {"clear state", []byte{2}, appr}, + } + for _, test := range tests { + ac = transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.UpdateApplicationOC, + ApprovalProgram: test.approval, + ClearStateProgram: test.clear, + } + + b.pass = true + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("updateApplication %s program too long", test.name)) + } + + // check extraProgramPages allows length of proto.MaxAppProgramLen + 1 + appr = make([]byte, proto.MaxAppProgramLen+1) + appr[0] = 4 + ac = transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.UpdateApplicationOC, + ApprovalProgram: appr, + ClearStateProgram: []byte{2}, + } + b.pass = true + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + + // check extraProgramPages is used and long sum rejected + ac = transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.UpdateApplicationOC, + ApprovalProgram: appr, + ClearStateProgram: appr, + } + b.pass = true + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.Error(err) + a.Contains(err.Error(), "updateApplication app programs too long") } func TestAppCallApplyDelete(t *testing.T) { @@ -981,6 +1059,7 @@ func TestAppCallApplyDelete(t *testing.T) { StateSchemas: basics.StateSchemas{ GlobalStateSchema: basics.StateSchema{NumUint: 1}, }, + ExtraProgramPages: 1, } h := transactions.Header{ Sender: sender, @@ -990,15 +1069,19 @@ func TestAppCallApplyDelete(t *testing.T) { var b testBalances b.balances = make(map[basics.Address]basics.AccountData) + // cbr is to ensure the original balance record is not modified but copied when updated in apply cbr := basics.AccountData{ - AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + TotalExtraAppPages: 1, } cp := basics.AccountData{ - AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + TotalExtraAppPages: 1, } b.balances[creator] = cp b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + // check if it fails nothing changes b.SetProto(protocol.ConsensusFuture) proto := b.ConsensusParams() ep.Proto = &proto @@ -1008,26 +1091,64 @@ func TestAppCallApplyDelete(t *testing.T) { a.Error(err) a.Contains(err.Error(), "transaction rejected by ApprovalProgram") a.Equal(0, b.put) - a.Equal(0, b.putWith) br := b.balances[creator] a.Equal(cbr, br) a.Equal(basics.EvalDelta{}, ad.EvalDelta) - // check deletion on empty balance record - happy case + // check calculation on ConsensusV28. TotalExtraAppPages does not change + b.SetProto(protocol.ConsensusV28) + proto = b.ConsensusParams() + ep.Proto = &proto + b.pass = true b.balances[sender] = basics.AccountData{} err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) a.NoError(err) a.Equal(appIdx, b.deAllocatedAppIdx) - a.Equal(0, b.put) - a.Equal(1, b.putWith) + a.Equal(1, b.put) br = b.balances[creator] a.Equal(cbr, br) br = b.putBalances[creator] a.Equal(basics.AppParams{}, br.AppParams[appIdx]) a.Equal(basics.StateSchema{}, br.TotalAppSchema) a.Equal(basics.EvalDelta{}, ad.EvalDelta) - a.Equal(uint32(0), br.TotalExtraAppPages) + a.Equal(uint32(1), br.TotalExtraAppPages) + b.ResetWrites() + + b.SetProto(protocol.ConsensusFuture) + proto = b.ConsensusParams() + ep.Proto = &proto + + // check deletion + for initTotalExtraPages := uint32(0); initTotalExtraPages < 3; initTotalExtraPages++ { + cbr = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + TotalExtraAppPages: initTotalExtraPages, + } + cp := basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + TotalExtraAppPages: initTotalExtraPages, + } + b.balances[creator] = cp + b.pass = true + b.balances[sender] = basics.AccountData{} + err = ApplicationCall(ac, h, &b, ad, &ep, txnCounter) + a.NoError(err) + a.Equal(appIdx, b.deAllocatedAppIdx) + a.Equal(1, b.put) + br = b.balances[creator] + a.Equal(cbr, br) + br = b.putBalances[creator] + a.Equal(basics.AppParams{}, br.AppParams[appIdx]) + a.Equal(basics.StateSchema{}, br.TotalAppSchema) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + if initTotalExtraPages <= params.ExtraProgramPages { + a.Equal(uint32(0), br.TotalExtraAppPages) + } else { + a.Equal(initTotalExtraPages-1, br.TotalExtraAppPages) + } + b.ResetWrites() + } } func TestAppCallApplyCreateClearState(t *testing.T) { diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go index e7a01c6cf..739d7803c 100644 --- a/ledger/apply/apply.go +++ b/ledger/apply/apply.go @@ -33,21 +33,22 @@ type Balances interface { Put(basics.Address, basics.AccountData) error - // PutWithCreatable is like Put, but should be used when creating or deleting an asset or application. - PutWithCreatable(addr basics.Address, acct basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error - // GetCreator gets the address of the account that created a given creatable GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) - // Allocate or Deallocate either global or address-local app storage. + // Allocate or deallocate either global or address-local app storage. // - // PutWithCreatable(...) and then {Allocate/Deallocate}(..., ..., global=true) + // Put(...) and then {AllocateApp/DeallocateApp}(..., ..., global=true) // creates/destroys an application. // - // Put(...) and then {Allocate/Deallocate}(..., ..., global=false) + // Put(...) and then {AllocateApp/DeallocateApp}(..., ..., global=false) // opts into/closes out of an application. - Allocate(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error - Deallocate(addr basics.Address, aidx basics.AppIndex, global bool) error + AllocateApp(addr basics.Address, aidx basics.AppIndex, global bool, space basics.StateSchema) error + DeallocateApp(addr basics.Address, aidx basics.AppIndex, global bool) error + + // Similar to above, notify COW that global/local asset state was created. + AllocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error + DeallocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error // StatefulEval executes a TEAL program in stateful mode on the balances. // It returns whether the program passed and its error. It alo returns diff --git a/ledger/apply/asset.go b/ledger/apply/asset.go index 26d50acf2..4c74649b7 100644 --- a/ledger/apply/asset.go +++ b/ledger/apply/asset.go @@ -95,14 +95,13 @@ func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Heade return fmt.Errorf("too many assets in account: %d > %d", len(record.Assets), balances.ConsensusParams().MaxAssetsPerAccount) } - // Tell the cow what asset we created - created := &basics.CreatableLocator{ - Creator: header.Sender, - Type: basics.AssetCreatable, - Index: basics.CreatableIndex(newidx), + err = balances.Put(header.Sender, record) + if err != nil { + return err } - return balances.PutWithCreatable(header.Sender, record, created, nil) + // Tell the cow what asset we created + return balances.AllocateAsset(header.Sender, newidx, true) } // Re-configuration and destroying must be done by the manager key. @@ -123,7 +122,6 @@ func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Heade record.Assets = cloneAssetHoldings(record.Assets) record.AssetParams = cloneAssetParams(record.AssetParams) - var deleted *basics.CreatableLocator if cc.AssetParams == (basics.AssetParams{}) { // Destroying an asset. The creator account must hold // the entire outstanding asset amount. @@ -132,10 +130,9 @@ func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Heade } // Tell the cow what asset we deleted - deleted = &basics.CreatableLocator{ - Creator: creator, - Type: basics.AssetCreatable, - Index: basics.CreatableIndex(cc.ConfigAsset), + err = balances.DeallocateAsset(creator, cc.ConfigAsset, true) + if err != nil { + return err } delete(record.Assets, cc.ConfigAsset) @@ -158,7 +155,7 @@ func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Heade record.AssetParams[cc.ConfigAsset] = params } - return balances.PutWithCreatable(creator, record, nil, deleted) + return balances.Put(creator, record) } func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { diff --git a/ledger/apply/keyreg_test.go b/ledger/apply/keyreg_test.go index ce9185300..58e4e8f68 100644 --- a/ledger/apply/keyreg_test.go +++ b/ledger/apply/keyreg_test.go @@ -49,10 +49,6 @@ func (balances keyregTestBalances) Put(addr basics.Address, ad basics.AccountDat return nil } -func (balances keyregTestBalances) PutWithCreatable(basics.Address, basics.AccountData, *basics.CreatableLocator, *basics.CreatableLocator) error { - return nil -} - func (balances keyregTestBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards, dstRewards *basics.MicroAlgos) error { return nil } @@ -65,11 +61,19 @@ func (balances keyregTestBalances) Round() basics.Round { return basics.Round(4294967296) } -func (balances keyregTestBalances) Allocate(basics.Address, basics.AppIndex, bool, basics.StateSchema) error { +func (balances keyregTestBalances) AllocateApp(basics.Address, basics.AppIndex, bool, basics.StateSchema) error { + return nil +} + +func (balances keyregTestBalances) DeallocateApp(basics.Address, basics.AppIndex, bool) error { + return nil +} + +func (balances keyregTestBalances) AllocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { return nil } -func (balances keyregTestBalances) Deallocate(basics.Address, basics.AppIndex, bool) error { +func (balances keyregTestBalances) DeallocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { return nil } diff --git a/ledger/apply/mockBalances_test.go b/ledger/apply/mockBalances_test.go index fadc97fe1..a1b64a2f2 100644 --- a/ledger/apply/mockBalances_test.go +++ b/ledger/apply/mockBalances_test.go @@ -49,22 +49,26 @@ func (balances mockBalances) Round() basics.Round { return basics.Round(8675309) } -func (balances mockBalances) Allocate(basics.Address, basics.AppIndex, bool, basics.StateSchema) error { +func (balances mockBalances) AllocateApp(basics.Address, basics.AppIndex, bool, basics.StateSchema) error { return nil } -func (balances mockBalances) Deallocate(basics.Address, basics.AppIndex, bool) error { +func (balances mockBalances) DeallocateApp(basics.Address, basics.AppIndex, bool) error { return nil } -func (balances mockBalances) StatefulEval(logic.EvalParams, basics.AppIndex, []byte) (bool, basics.EvalDelta, error) { - return false, basics.EvalDelta{}, nil +func (balances mockBalances) AllocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { + return nil } -func (balances mockBalances) PutWithCreatable(basics.Address, basics.AccountData, *basics.CreatableLocator, *basics.CreatableLocator) error { +func (balances mockBalances) DeallocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { return nil } +func (balances mockBalances) StatefulEval(logic.EvalParams, basics.AppIndex, []byte) (bool, basics.EvalDelta, error) { + return false, basics.EvalDelta{}, nil +} + func (balances mockBalances) Get(addr basics.Address, withPendingRewards bool) (basics.AccountData, error) { return balances.b[addr], nil } diff --git a/ledger/assetcow.go b/ledger/assetcow.go new file mode 100644 index 000000000..d84ae3221 --- /dev/null +++ b/ledger/assetcow.go @@ -0,0 +1,48 @@ +// Copyright (C) 2019-2021 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package ledger + +import ( + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger/ledgercore" +) + +func (cs *roundCowState) AllocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { + if global { + cs.mods.Creatables[basics.CreatableIndex(index)] = ledgercore.ModifiedCreatable{ + Ctype: basics.AssetCreatable, + Creator: addr, + Created: true, + } + + cs.trackCreatable(basics.CreatableIndex(index)) + } + + return nil +} + +func (cs *roundCowState) DeallocateAsset(addr basics.Address, index basics.AssetIndex, global bool) error { + if global { + cs.mods.Creatables[basics.CreatableIndex(index)] = ledgercore.ModifiedCreatable{ + Ctype: basics.AssetCreatable, + Creator: addr, + Created: false, + } + } + + return nil +} diff --git a/ledger/cow.go b/ledger/cow.go index ba17dafc5..56ff9e33b 100644 --- a/ledger/cow.go +++ b/ledger/cow.go @@ -194,26 +194,6 @@ func (cb *roundCowState) blockHdr(r basics.Round) (bookkeeping.BlockHeader, erro return cb.lookupParent.blockHdr(r) } -func (cb *roundCowState) put(addr basics.Address, new basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) { - cb.mods.Accts.Upsert(addr, new) - - if newCreatable != nil { - cb.mods.Creatables[newCreatable.Index] = ledgercore.ModifiedCreatable{ - Ctype: newCreatable.Type, - Creator: newCreatable.Creator, - Created: true, - } - } - - if deletedCreatable != nil { - cb.mods.Creatables[deletedCreatable.Index] = ledgercore.ModifiedCreatable{ - Ctype: deletedCreatable.Type, - Creator: deletedCreatable.Creator, - Created: false, - } - } -} - func (cb *roundCowState) trackCreatable(creatableIndex basics.CreatableIndex) { cb.trackedCreatables[cb.groupIdx] = creatableIndex } diff --git a/ledger/cow_test.go b/ledger/cow_test.go index b09c479e8..71e12530b 100644 --- a/ledger/cow_test.go +++ b/ledger/cow_test.go @@ -101,7 +101,7 @@ func checkCow(t *testing.T, cow *roundCowState, accts map[basics.Address]basics. func applyUpdates(cow *roundCowState, updates ledgercore.AccountDeltas) { for i := 0; i < updates.Len(); i++ { addr, delta := updates.GetByIdx(i) - cow.put(addr, delta, nil, nil) + cow.Put(addr, delta) } } diff --git a/ledger/eval.go b/ledger/eval.go index 3b1968a74..f75d7a57e 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -247,16 +247,7 @@ func (cs *roundCowState) GetCreator(cidx basics.CreatableIndex, ctype basics.Cre } func (cs *roundCowState) Put(addr basics.Address, acct basics.AccountData) error { - return cs.PutWithCreatable(addr, acct, nil, nil) -} - -func (cs *roundCowState) PutWithCreatable(addr basics.Address, acct basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { - cs.put(addr, acct, newCreatable, deletedCreatable) - - // store the creatable locator - if newCreatable != nil { - cs.trackCreatable(newCreatable.Index) - } + cs.mods.Accts.Upsert(addr, acct) return nil } @@ -283,7 +274,7 @@ func (cs *roundCowState) Move(from basics.Address, to basics.Address, amt basics if overflowed { return fmt.Errorf("overspend (account %v, data %+v, tried to spend %v)", from, fromBal, amt) } - cs.put(from, fromBalNew, nil, nil) + cs.Put(from, fromBalNew) toBal, err := cs.lookup(to) if err != nil { @@ -304,7 +295,7 @@ func (cs *roundCowState) Move(from basics.Address, to basics.Address, amt basics if overflowed { return fmt.Errorf("balance overflow (account %v, data %+v, was going to receive %v)", to, toBal, amt) } - cs.put(to, toBalNew, nil, nil) + cs.Put(to, toBalNew) return nil } diff --git a/libgoal/libgoal.go b/libgoal/libgoal.go index 87cfcbaa6..7fe882b0f 100644 --- a/libgoal/libgoal.go +++ b/libgoal/libgoal.go @@ -746,6 +746,16 @@ func (c *Client) PendingTransactionInformation(txid string) (resp v1.Transaction return } +// PendingTransactionInformationV2 returns information about a recently issued +// transaction based on its txid. +func (c *Client) PendingTransactionInformationV2(txid string) (resp generatedV2.PendingTransactionResponse, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + resp, err = algod.PendingTransactionInformationV2(txid) + } + return +} + // Block takes a round and returns its block func (c *Client) Block(round uint64) (resp v1.Block, err error) { algod, err := c.ensureAlgodClient() diff --git a/protocol/consensus.go b/protocol/consensus.go index 52c38c002..47e6148eb 100644 --- a/protocol/consensus.go +++ b/protocol/consensus.go @@ -153,6 +153,11 @@ const ConsensusV28 = ConsensusVersion( "https://github.com/algorandfoundation/specs/tree/65b4ab3266c52c56a0fa7d591754887d68faad0a", ) +// ConsensusV29 fixes application update by using ExtraProgramPages in size calculations +const ConsensusV29 = ConsensusVersion( + "https://github.com/algorandfoundation/specs/tree/abc54f79f9ad679d2d22f0fb9909fb005c16f8a1", +) + // 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( @@ -165,7 +170,7 @@ const ConsensusFuture = ConsensusVersion( // ConsensusCurrentVersion is the latest version and should be used // when a specific version is not provided. -const ConsensusCurrentVersion = ConsensusV28 +const ConsensusCurrentVersion = ConsensusV29 // Error is used to indicate that an unsupported protocol has been detected. type Error ConsensusVersion diff --git a/scripts/travis/run_tests.sh b/scripts/travis/run_tests.sh index 0c6f278f4..cf47b0b97 100755 --- a/scripts/travis/run_tests.sh +++ b/scripts/travis/run_tests.sh @@ -4,14 +4,14 @@ set -e if [ "${BUILD_TYPE}" = "integration" ]; then # Run short tests when doing pull requests; leave the long testing for nightly runs. - if [[ "${TRAVIS_BRANCH}" =~ ^rel/nightly ]]; then + if [[ "${TRAVIS_BRANCH}" =~ ^rel/nightly ]] || [[ "${TRAVIS_BRANCH}" =~ ^hotfix/ ]]; then SHORTTEST= else SHORTTEST=-short fi export SHORTTEST make integration -elif [ "${TRAVIS_EVENT_TYPE}" = "cron" ] || [[ "${TRAVIS_BRANCH}" =~ ^rel/ ]]; then +elif [ "${TRAVIS_EVENT_TYPE}" = "cron" ] || [[ "${TRAVIS_BRANCH}" =~ ^rel/ ]] || [[ "${TRAVIS_BRANCH}" =~ ^hotfix/ ]]; then make fulltest -j2 else make shorttest -j2 diff --git a/test/e2e-go/features/transactions/app_pages_test.go b/test/e2e-go/features/transactions/app_pages_test.go new file mode 100644 index 000000000..17983c1e2 --- /dev/null +++ b/test/e2e-go/features/transactions/app_pages_test.go @@ -0,0 +1,183 @@ +// Copyright (C) 2019-2021 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package transactions + +import ( + "encoding/base64" + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/test/framework/fixtures" +) + +func TestExtraProgramPages(t *testing.T) { + t.Parallel() + a := require.New(fixtures.SynchronizedTest(t)) + + var fixture fixtures.RestClientFixture + fixture.Setup(t, filepath.Join("nettemplates", "TwoNodes50EachFuture.json")) + defer fixture.Shutdown() + client := fixture.LibGoalClient + + accountList, err := fixture.GetWalletsSortedByBalance() + a.NoError(err) + baseAcct := accountList[0].Address + + walletHandle, err := client.GetUnencryptedWalletHandle() + a.NoError(err) + + accountInfo, err := client.AccountInformationV2(baseAcct) + a.NoError(err) + if accountInfo.AppsTotalExtraPages != nil { + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(0)) + } + + var inconsequentialBytes [2048]byte + srcBigProgram := fmt.Sprintf(`#pragma version 4 +byte base64(%s) +pop +int 1 +return +`, base64.StdEncoding.EncodeToString(inconsequentialBytes[:])) + + srcSmallProgram := `#pragma version 4 +int 1 +return +` + + bigProgramOps, err := logic.AssembleString(srcBigProgram) + a.NoError(err) + bigProgram := bigProgramOps.Program + + smallProgramOps, err := logic.AssembleString(srcSmallProgram) + a.NoError(err) + smallProgram := smallProgramOps.Program + + globalSchema := basics.StateSchema{ + NumByteSlice: 1, + } + localSchema := basics.StateSchema{ + NumByteSlice: 1, + } + + status, err := client.Status() + a.NoError(err) + + // create app 1 with 1 extra page + app1ExtraPages := uint32(1) + tx, err := client.MakeUnsignedAppCreateTx(transactions.NoOpOC, smallProgram, smallProgram, globalSchema, localSchema, nil, nil, nil, nil, app1ExtraPages) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(baseAcct, 0, 0, 0, tx) + a.NoError(err) + txid, err := client.SignAndBroadcastTransaction(walletHandle, nil, tx) + a.NoError(err) + _, err = fixture.WaitForConfirmedTxn(status.LastRound+5, baseAcct, txid) + a.NoError(err) + + app1CreateTxn, err := client.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(app1CreateTxn.ConfirmedRound) + a.NotNil(app1CreateTxn.ApplicationIndex) + app1ID := *app1CreateTxn.ApplicationIndex + + accountInfo, err = client.AccountInformationV2(baseAcct) + a.NoError(err) + a.NotNil(accountInfo.AppsTotalExtraPages) + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(app1ExtraPages)) + + // update app 1 and ensure the extra page still works + tx, err = client.MakeUnsignedAppUpdateTx(app1ID, nil, nil, nil, nil, bigProgram, smallProgram) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(baseAcct, 0, 0, 0, tx) + a.NoError(err) + txid, err = client.SignAndBroadcastTransaction(walletHandle, nil, tx) + a.NoError(err) + _, err = fixture.WaitForConfirmedTxn(*app1CreateTxn.ConfirmedRound+5, baseAcct, txid) + a.NoError(err) + + app1UpdateTxn, err := client.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(app1CreateTxn.ConfirmedRound) + + accountInfo, err = client.AccountInformationV2(baseAcct) + a.NoError(err) + a.NotNil(accountInfo.AppsTotalExtraPages) + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(app1ExtraPages)) + + // create app 2 with 2 extra pages + app2ExtraPages := uint32(2) + tx, err = client.MakeUnsignedAppCreateTx(transactions.NoOpOC, bigProgram, smallProgram, globalSchema, localSchema, nil, nil, nil, nil, app2ExtraPages) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(baseAcct, 0, 0, 0, tx) + a.NoError(err) + txid, err = client.SignAndBroadcastTransaction(walletHandle, nil, tx) + a.NoError(err) + _, err = fixture.WaitForConfirmedTxn(*app1UpdateTxn.ConfirmedRound+5, baseAcct, txid) + a.NoError(err) + + app2CreateTxn, err := client.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(app2CreateTxn.ConfirmedRound) + a.NotNil(app2CreateTxn.ApplicationIndex) + app2ID := *app2CreateTxn.ApplicationIndex + + accountInfo, err = client.AccountInformationV2(baseAcct) + a.NoError(err) + a.NotNil(accountInfo.AppsTotalExtraPages) + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(app1ExtraPages+app2ExtraPages)) + + // delete app 1 + tx, err = client.MakeUnsignedAppDeleteTx(app1ID, nil, nil, nil, nil) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(baseAcct, 0, 0, 0, tx) + a.NoError(err) + txid, err = client.SignAndBroadcastTransaction(walletHandle, nil, tx) + a.NoError(err) + _, err = fixture.WaitForConfirmedTxn(*app2CreateTxn.ConfirmedRound+5, baseAcct, txid) + a.NoError(err) + + app1DeleteTxn, err := client.PendingTransactionInformationV2(txid) + a.NoError(err) + a.NotNil(app1DeleteTxn.ConfirmedRound) + + accountInfo, err = client.AccountInformationV2(baseAcct) + a.NoError(err) + a.NotNil(accountInfo.AppsTotalExtraPages) + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(app2ExtraPages)) + + // delete app 2 + tx, err = client.MakeUnsignedAppDeleteTx(app2ID, nil, nil, nil, nil) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(baseAcct, 0, 0, 0, tx) + a.NoError(err) + txid, err = client.SignAndBroadcastTransaction(walletHandle, nil, tx) + a.NoError(err) + _, err = fixture.WaitForConfirmedTxn(*app1DeleteTxn.ConfirmedRound+5, baseAcct, txid) + a.NoError(err) + + accountInfo, err = client.AccountInformationV2(baseAcct) + a.NoError(err) + if accountInfo.AppsTotalExtraPages != nil { + a.Equal(*accountInfo.AppsTotalExtraPages, uint64(0)) + } +} diff --git a/test/scripts/e2e_subs/e2e-app-extra-pages.sh b/test/scripts/e2e_subs/e2e-app-extra-pages.sh index 0fafa3256..9c32cef59 100755 --- a/test/scripts/e2e_subs/e2e-app-extra-pages.sh +++ b/test/scripts/e2e_subs/e2e-app-extra-pages.sh @@ -42,10 +42,14 @@ function generate_teal() { BIG_TEAL_FILE="$TEMPDIR/big-app.teal" BIG_TEAL_V4_FILE="$TEMPDIR/big-app-v4.teal" SMALL_TEAL_FILE="$TEMPDIR/sm-app.teal" +APPR_PROG="$TEMPDIR/appr-prog.teal" +BIG_APPR_PROG="$TEMPDIR/big-appr-prog.teal" generate_teal "$BIG_TEAL_FILE" 3 4090 1 "int 0\nbalance\npop\n" generate_teal "$BIG_TEAL_V4_FILE" 4 4090 1 "int 0\nbalance\npop\n" generate_teal "$SMALL_TEAL_FILE" 3 10 1 "int 0\nbalance\npop\n" +generate_teal "$APPR_PROG" 4 3072 1 "int 0\nbalance\npop\n" +generate_teal "$BIG_APPR_PROG" 4 4098 1 "int 0\nbalance\npop\n" # App create fails. Approval program too long RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${BIG_TEAL_FILE}" --clear-prog "${BIG_TEAL_FILE}" --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) @@ -79,5 +83,34 @@ if [[ $RES != *"${EXPERROR}"* ]]; then false fi -# App create with extra pages, succeedd +# App create with extra pages, succeeded ${gcmd} app create --creator ${ACCOUNT} --approval-prog "${SMALL_TEAL_FILE}" --clear-prog "${SMALL_TEAL_FILE}" --extra-pages 1 --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 + +# App update +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${SMALL_TEAL_FILE}" --clear-prog "${SMALL_TEAL_FILE}" --extra-pages 1 --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) +EXP="Created app" +APPID=$(echo $RES | awk '{print $NF}') +if [[ $RES != *"${EXP}"* ]]; then + date '+app-extra-pages-test FAIL the application creation should pass %Y%m%d_%H%M%S' + false +fi + +RES=$(${gcmd} app info --app-id ${APPID} 2>&1 || true) +PROGHASH="Approval hash: 7356635AKR4FJOOKXXBWNN6HDJ5U3O2YWAOSK6NZBPMOGIQSWCL2N74VT4" +if [[ $RES != *"${PROGHASH}"* ]]; then + date '+app-extra-pages-test FAIL the application info should succeed %Y%m%d_%H%M%S' + false +fi + +RES=$(${gcmd} app update --app-id ${APPID} --approval-prog "${APPR_PROG}" --clear-prog "${SMALL_TEAL_FILE}" --from ${ACCOUNT} 2>&1 || true) +EXP="Attempting to update app" +if [[ $RES != *"${EXP}"* ]]; then + date '+app-extra-pages-test FAIL the application update should succeed %Y%m%d_%H%M%S' + false +fi + +RES=$(${gcmd} app info --app-id ${APPID} 2>&1 || true) +if [[ $RES == *"${PROGHASH}"* ]]; then + date '+app-extra-pages-test FAIL the application approval program should have been updated %Y%m%d_%H%M%S' + false +fi |