summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Lee <64482439+algojohnlee@users.noreply.github.com>2020-08-06 23:39:19 -0400
committerGitHub <noreply@github.com>2020-08-06 23:39:19 -0400
commit48422dd79d60676a689d0b296d0440e7b65c1e09 (patch)
treee095a3c0914074c7d65c2317fa369cd99abd5ca2
parentc845f99bd44dbba84b9a71b64660f5ba576dff1e (diff)
parent54a929254e669710268e3dbc5e3931948f8ffb9c (diff)
Merge pull request #1356 from onetechnical/onetechnical/relbeta2.1.1v2.1.1-beta
Onetechnical/relbeta2.1.1
-rw-r--r--agreement/abstractions.go5
-rw-r--r--agreement/pseudonode.go4
-rw-r--r--buildnumber.dat2
-rw-r--r--config/consensus_test.go19
-rw-r--r--crypto/merkletrie/cache.go13
-rw-r--r--crypto/merkletrie/cache_test.go224
-rw-r--r--crypto/merkletrie/trie_test.go1
-rw-r--r--daemon/algod/api/algod.oas2.json6
-rw-r--r--daemon/algod/api/algod.oas3.yml120
-rw-r--r--daemon/algod/api/server/v2/generated/routes.go313
-rw-r--r--data/pools/transactionPool.go9
-rw-r--r--debug/genconsensusconfig/main.go39
-rw-r--r--ledger/accountdb_test.go251
-rw-r--r--ledger/acctupdates_test.go13
-rw-r--r--ledger/ledger_test.go158
-rw-r--r--node/node.go15
-rwxr-xr-xscripts/release/build/build_algod_docker.sh4
-rw-r--r--test/e2e-go/upgrades/application_support_test.go1
-rw-r--r--test/e2e-go/upgrades/rekey_support_test.go125
-rwxr-xr-xtest/scripts/e2e_subs/rekey.sh106
20 files changed, 1217 insertions, 211 deletions
diff --git a/agreement/abstractions.go b/agreement/abstractions.go
index da1c5942e..f305cc230 100644
--- a/agreement/abstractions.go
+++ b/agreement/abstractions.go
@@ -18,6 +18,7 @@ package agreement
import (
"context"
+ "errors"
"time"
"github.com/algorand/go-algorand/config"
@@ -65,6 +66,10 @@ type ValidatedBlock interface {
Block() bookkeeping.Block
}
+// ErrAssembleBlockRoundStale is returned by AssembleBlock when the requested round number is not the
+// one that matches the ledger last committed round + 1.
+var ErrAssembleBlockRoundStale = errors.New("requested round for AssembleBlock is stale")
+
// An BlockFactory produces an Block which is suitable for proposal for a given
// Round.
type BlockFactory interface {
diff --git a/agreement/pseudonode.go b/agreement/pseudonode.go
index c2d7ec2b5..601bd3d34 100644
--- a/agreement/pseudonode.go
+++ b/agreement/pseudonode.go
@@ -266,7 +266,9 @@ func (n asyncPseudonode) makeProposals(round basics.Round, period period, accoun
deadline := time.Now().Add(AssemblyTime)
ve, err := n.factory.AssembleBlock(round, deadline)
if err != nil {
- n.log.Errorf("pseudonode.makeProposals: could not generate a proposal for round %d: %v", round, err)
+ if err != ErrAssembleBlockRoundStale {
+ n.log.Errorf("pseudonode.makeProposals: could not generate a proposal for round %d: %v", round, err)
+ }
return nil, nil
}
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_test.go b/config/consensus_test.go
index 7c34947d0..c0da43988 100644
--- a/config/consensus_test.go
+++ b/config/consensus_test.go
@@ -18,6 +18,8 @@ package config
import (
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestConsensusParams(t *testing.T) {
@@ -34,3 +36,20 @@ func TestConsensusParams(t *testing.T) {
}
}
}
+
+// TestConsensusUpgradeWindow ensures that the upgrade window is a non-zero value, and confirm to be within the valid range.
+func TestConsensusUpgradeWindow(t *testing.T) {
+ for proto, params := range Consensus {
+ require.GreaterOrEqualf(t, params.MaxUpgradeWaitRounds, params.MinUpgradeWaitRounds, "Version :%v", proto)
+ for toVersion, delay := range params.ApprovedUpgrades {
+ if params.MinUpgradeWaitRounds != 0 || params.MaxUpgradeWaitRounds != 0 {
+ require.NotZerof(t, delay, "From :%v\nTo :%v", proto, toVersion)
+ require.GreaterOrEqualf(t, delay, params.MinUpgradeWaitRounds, "From :%v\nTo :%v", proto, toVersion)
+ require.LessOrEqualf(t, delay, params.MaxUpgradeWaitRounds, "From :%v\nTo :%v", proto, toVersion)
+ } else {
+ require.Zerof(t, delay, "From :%v\nTo :%v", proto, toVersion)
+
+ }
+ }
+ }
+}
diff --git a/crypto/merkletrie/cache.go b/crypto/merkletrie/cache.go
index 9cba7c19a..3af7bc8b4 100644
--- a/crypto/merkletrie/cache.go
+++ b/crypto/merkletrie/cache.go
@@ -72,7 +72,7 @@ type merkleTrieCache struct {
// a list of the pages priorities. The item in the front has higher priority and would not get evicted as quickly as the item on the back
pagesPrioritizationList *list.List
- // the list element of each of the priorities
+ // the list element of each of the priorities. The pagesPrioritizationMap maps a page id to the page priority list element.
pagesPrioritizationMap map[uint64]*list.Element
// the page to load before the nextNodeID at init time. If zero, then nothing is being reloaded.
deferedPageLoad uint64
@@ -145,8 +145,10 @@ func (mtc *merkleTrieCache) getNode(nid storedNodeIdentifier) (pnode *node, err
return
}
-// prioritizeNode make sure to move the priorities of the pages according to
-// the accessed node identifier
+// prioritizeNode make sure to adjust the priority of the given node id.
+// nodes are prioritized based on the page the belong to.
+// a new page would be placed on front, and an older page would get moved
+// to the front.
func (mtc *merkleTrieCache) prioritizeNode(nid storedNodeIdentifier) {
page := uint64(nid) / uint64(mtc.nodesPerPage)
@@ -350,6 +352,9 @@ func (mtc *merkleTrieCache) commit() error {
element := mtc.pagesPrioritizationMap[uint64(page)]
if element != nil {
mtc.pagesPrioritizationList.Remove(element)
+ delete(mtc.pagesPrioritizationMap, uint64(page))
+ mtc.cachedNodeCount -= len(mtc.pageToNIDsPtr[uint64(page)])
+ delete(mtc.pageToNIDsPtr, uint64(page))
}
}
@@ -440,6 +445,7 @@ func (mtc *merkleTrieCache) encodePage(nodeIDs map[storedNodeIdentifier]*node) [
// evict releases the least used pages from cache until the number of elements in cache are less than cachedNodeCountTarget
func (mtc *merkleTrieCache) evict() (removedNodes int) {
removedNodes = mtc.cachedNodeCount
+
for mtc.cachedNodeCount > mtc.cachedNodeCountTarget {
// get the least used page off the pagesPrioritizationList
element := mtc.pagesPrioritizationList.Back()
@@ -448,6 +454,7 @@ func (mtc *merkleTrieCache) evict() (removedNodes int) {
}
mtc.pagesPrioritizationList.Remove(element)
pageToRemove := element.Value.(uint64)
+ delete(mtc.pagesPrioritizationMap, pageToRemove)
mtc.cachedNodeCount -= len(mtc.pageToNIDsPtr[pageToRemove])
delete(mtc.pageToNIDsPtr, pageToRemove)
}
diff --git a/crypto/merkletrie/cache_test.go b/crypto/merkletrie/cache_test.go
new file mode 100644
index 000000000..924063fdf
--- /dev/null
+++ b/crypto/merkletrie/cache_test.go
@@ -0,0 +1,224 @@
+// Copyright (C) 2019-2020 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 merkletrie
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/algorand/go-algorand/crypto"
+)
+
+func verifyCacheNodeCount(t *testing.T, trie *Trie) {
+ count := 0
+ for _, pageNodes := range trie.cache.pageToNIDsPtr {
+ count += len(pageNodes)
+ }
+ require.Equal(t, count, trie.cache.cachedNodeCount)
+
+ // make sure that the pagesPrioritizationMap aligns with pagesPrioritizationList
+ require.Equal(t, len(trie.cache.pagesPrioritizationMap), trie.cache.pagesPrioritizationList.Len())
+
+ // if we're not within a transaction, the following should also hold true:
+ if !trie.cache.modified {
+ require.Equal(t, len(trie.cache.pageToNIDsPtr), trie.cache.pagesPrioritizationList.Len())
+ }
+
+ for e := trie.cache.pagesPrioritizationList.Back(); e != nil; e = e.Next() {
+ page := e.Value.(uint64)
+ _, has := trie.cache.pagesPrioritizationMap[page]
+ require.True(t, has)
+ _, has = trie.cache.pageToNIDsPtr[page]
+ require.True(t, has)
+ }
+}
+
+func TestCacheEviction1(t *testing.T) {
+ var memoryCommitter InMemoryCommitter
+ mt1, _ := MakeTrie(&memoryCommitter, defaultTestEvictSize)
+ // create 13000 hashes.
+ leafsCount := 13000
+ hashes := make([]crypto.Digest, leafsCount)
+ for i := 0; i < len(hashes); i++ {
+ hashes[i] = crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})
+ }
+
+ for i := 0; i < defaultTestEvictSize; i++ {
+ mt1.Add(hashes[i][:])
+ }
+
+ for i := defaultTestEvictSize; i < len(hashes); i++ {
+ mt1.Add(hashes[i][:])
+ mt1.Evict(true)
+ require.GreaterOrEqual(t, defaultTestEvictSize, mt1.cache.cachedNodeCount)
+ verifyCacheNodeCount(t, mt1)
+ }
+}
+
+func TestCacheEviction2(t *testing.T) {
+ var memoryCommitter InMemoryCommitter
+ mt1, _ := MakeTrie(&memoryCommitter, defaultTestEvictSize)
+ // create 20000 hashes.
+ leafsCount := 20000
+ hashes := make([]crypto.Digest, leafsCount)
+ for i := 0; i < len(hashes); i++ {
+ hashes[i] = crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})
+ }
+
+ for i := 0; i < defaultTestEvictSize; i++ {
+ mt1.Add(hashes[i][:])
+ }
+
+ for i := defaultTestEvictSize; i < len(hashes); i++ {
+ mt1.Delete(hashes[i-2][:])
+ mt1.Add(hashes[i][:])
+ mt1.Add(hashes[i-2][:])
+
+ if i%(len(hashes)/20) == 0 {
+ mt1.Evict(true)
+ require.GreaterOrEqual(t, defaultTestEvictSize, mt1.cache.cachedNodeCount)
+ verifyCacheNodeCount(t, mt1)
+ }
+ }
+}
+
+func TestCacheEviction3(t *testing.T) {
+ var memoryCommitter InMemoryCommitter
+ mt1, _ := MakeTrie(&memoryCommitter, defaultTestEvictSize)
+ // create 200000 hashes.
+ leafsCount := 200000
+ hashes := make([]crypto.Digest, leafsCount)
+ for i := 0; i < len(hashes); i++ {
+ hashes[i] = crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})
+ }
+
+ for i := 0; i < defaultTestEvictSize; i++ {
+ mt1.Add(hashes[i][:])
+ }
+
+ for i := defaultTestEvictSize; i < len(hashes); i++ {
+ mt1.Delete(hashes[i-500][:])
+ mt1.Add(hashes[i][:])
+
+ if i%(len(hashes)/20) == 0 {
+ mt1.Evict(true)
+ require.GreaterOrEqual(t, defaultTestEvictSize, mt1.cache.cachedNodeCount)
+ verifyCacheNodeCount(t, mt1)
+ }
+ }
+}
+
+// smallPageMemoryCommitter is an InMemoryCommitter, which has a custom page size, and knows how to "fail" per request.
+type smallPageMemoryCommitter struct {
+ InMemoryCommitter
+ pageSize int64
+ failStore int
+ failLoad int
+}
+
+// GetNodesCountPerPage returns the page size ( number of nodes per page )
+func (spmc *smallPageMemoryCommitter) GetNodesCountPerPage() (pageSize int64) {
+ return spmc.pageSize
+}
+
+// StorePage stores a single page in an in-memory persistence.
+func (spmc *smallPageMemoryCommitter) StorePage(page uint64, content []byte) error {
+ if spmc.failStore > 0 {
+ spmc.failStore--
+ return fmt.Errorf("failStore>0")
+ }
+ return spmc.InMemoryCommitter.StorePage(page, content)
+}
+
+// LoadPage load a single page from an in-memory persistence.
+func (spmc *smallPageMemoryCommitter) LoadPage(page uint64) (content []byte, err error) {
+ if spmc.failLoad > 0 {
+ spmc.failLoad--
+ return nil, fmt.Errorf("failLoad>0")
+ }
+ return spmc.InMemoryCommitter.LoadPage(page)
+}
+
+func cacheEvictionFuzzer(t *testing.T, hashes []crypto.Digest, pageSize int64, evictSize int) {
+ var memoryCommitter smallPageMemoryCommitter
+ memoryCommitter.pageSize = pageSize
+ mt1, _ := MakeTrie(&memoryCommitter, evictSize)
+
+ // add the first 10 hashes.
+ for i := 0; i < 10; i++ {
+ mt1.Add(hashes[i][:])
+ }
+
+ for i := 10; i < len(hashes)-10; i++ {
+ for k := 0; k < int(hashes[i-2][0]%5); k++ {
+ if hashes[i+k-3][0]%7 == 0 {
+ memoryCommitter.failLoad++
+ }
+ if hashes[i+k-4][0]%7 == 0 {
+ memoryCommitter.failStore++
+ }
+ if hashes[i+k][0]%7 == 0 {
+ mt1.Delete(hashes[i+k-int(hashes[i][0]%7)][:])
+ }
+ mt1.Add(hashes[i+k+3-int(hashes[i+k-1][0]%7)][:])
+ }
+ if hashes[i][0]%5 == 0 {
+ verifyCacheNodeCount(t, mt1)
+ mt1.Evict(true)
+ verifyCacheNodeCount(t, mt1)
+ }
+ }
+}
+
+// TestCacheEvictionFuzzer generates bursts of random Add/Delete operations on the trie, and
+// testing the correctness of the cache internal buffers priodically.
+func TestCacheEvictionFuzzer(t *testing.T) {
+ // create 2000 hashes.
+ leafsCount := 2000
+ hashes := make([]crypto.Digest, leafsCount)
+ for i := 0; i < len(hashes); i++ {
+ hashes[i] = crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})
+ }
+ for _, pageSize := range []int64{2, 3, 8, 12, 17} {
+ for _, evictSize := range []int{5, 10, 13, 30} {
+ t.Run(fmt.Sprintf("Fuzzer-%d-%d", pageSize, evictSize), func(t *testing.T) {
+ cacheEvictionFuzzer(t, hashes, pageSize, evictSize)
+ })
+ }
+ }
+}
+
+// TestCacheEvictionFuzzer generates bursts of random Add/Delete operations on the trie, and
+// testing the correctness of the cache internal buffers priodically.
+func TestCacheEvictionFuzzer2(t *testing.T) {
+ // create 1000 hashes.
+ leafsCount := 1000
+ hashes := make([]crypto.Digest, leafsCount)
+ for i := 0; i < len(hashes); i++ {
+ hashes[i] = crypto.Hash([]byte{byte(i % 256), byte((i / 256) % 256), byte(i / 65536)})
+ }
+ for i := 0; i < 80; i++ {
+ pageSize := int64(1 + crypto.RandUint64()%101)
+ evictSize := int(1 + crypto.RandUint64()%37)
+ hashesCount := uint64(100) + crypto.RandUint64()%uint64(leafsCount-100)
+ t.Run(fmt.Sprintf("Fuzzer-%d-%d", pageSize, evictSize), func(t *testing.T) {
+ cacheEvictionFuzzer(t, hashes[:hashesCount], pageSize, evictSize)
+ })
+ }
+}
diff --git a/crypto/merkletrie/trie_test.go b/crypto/merkletrie/trie_test.go
index fe6f9fc18..171e23529 100644
--- a/crypto/merkletrie/trie_test.go
+++ b/crypto/merkletrie/trie_test.go
@@ -133,6 +133,7 @@ func TestRandomAddingAndRemoving(t *testing.T) {
if (i % (1 + int(processesHash[0]))) == 42 {
err := mt.Commit()
require.NoError(t, err)
+ verifyCacheNodeCount(t, mt)
}
}
}
diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json
index bf7b88a1c..8210a5d66 100644
--- a/daemon/algod/api/algod.oas2.json
+++ b/daemon/algod/api/algod.oas2.json
@@ -170,7 +170,8 @@
"get": {
"description": "Get the list of pending transactions by address, sorted by priority, in decreasing order, truncated at the end at MAX. If MAX = 0, returns all pending transactions.\n",
"produces": [
- "application/json"
+ "application/json",
+ "application/msgpack"
],
"schemes": [
"http"
@@ -663,7 +664,8 @@
"get": {
"description": "Given a transaction id of a recently submitted transaction, it returns information about it. There are several cases when this might succeed:\n- transaction committed (committed round \u003e 0) - transaction still in the pool (committed round = 0, pool error = \"\") - transaction removed from pool due to error (committed round = 0, pool error != \"\")\nOr the transaction may have happened sufficiently long ago that the node no longer remembers it, and this will return an error.\n",
"produces": [
- "application/json"
+ "application/json",
+ "application/msgpack"
],
"schemes": [
"http"
diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml
index 2c3c98390..688ee9c26 100644
--- a/daemon/algod/api/algod.oas3.yml
+++ b/daemon/algod/api/algod.oas3.yml
@@ -1583,6 +1583,31 @@
],
"type": "object"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "description": "PendingTransactions is an array of signed transactions exactly as they were submitted.",
+ "properties": {
+ "top-transactions": {
+ "description": "An array of signed transaction objects.",
+ "items": {
+ "properties": {},
+ "type": "object",
+ "x-algorand-format": "SignedTransaction"
+ },
+ "type": "array"
+ },
+ "total-transactions": {
+ "description": "Total number of transactions in the pool.",
+ "type": "integer"
+ }
+ },
+ "required": [
+ "top-transactions",
+ "total-transactions"
+ ],
+ "type": "object"
+ }
}
},
"description": "A potentially truncated list of transactions currently in the node's transaction pool. You can compute whether or not the list is truncated if the number of elements in the **top-transactions** array is fewer than **total-transactions**."
@@ -1593,6 +1618,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Max must be a non-negative integer"
@@ -1603,6 +1633,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Invalid API Token"
@@ -1613,6 +1648,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Internal Error"
@@ -1623,6 +1663,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Service Temporarily Unavailable"
@@ -3069,6 +3114,66 @@
],
"type": "object"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "description": "Details about a pending transaction. If the transaction was recently confirmed, includes confirmation details like the round and reward details.",
+ "properties": {
+ "application-index": {
+ "description": "The application index if the transaction was found and it created an application.",
+ "type": "integer"
+ },
+ "asset-index": {
+ "description": "The asset index if the transaction was found and it created an asset.",
+ "type": "integer"
+ },
+ "close-rewards": {
+ "description": "Rewards in microalgos applied to the close remainder to account.",
+ "type": "integer"
+ },
+ "closing-amount": {
+ "description": "Closing amount for the transaction.",
+ "type": "integer"
+ },
+ "confirmed-round": {
+ "description": "The round where this transaction was confirmed, if present.",
+ "type": "integer"
+ },
+ "global-state-delta": {
+ "$ref": "#/components/schemas/StateDelta"
+ },
+ "local-state-delta": {
+ "description": "\\[ld\\] Local state key/value changes for the application being executed by this transaction.",
+ "items": {
+ "$ref": "#/components/schemas/AccountStateDelta"
+ },
+ "type": "array"
+ },
+ "pool-error": {
+ "description": "Indicates that the transaction was kicked out of this node's transaction pool (and specifies why that happened). An empty string indicates the transaction wasn't kicked out of this node's txpool due to an error.\n",
+ "type": "string"
+ },
+ "receiver-rewards": {
+ "description": "Rewards in microalgos applied to the receiver account.",
+ "type": "integer"
+ },
+ "sender-rewards": {
+ "description": "Rewards in microalgos applied to the sender account.",
+ "type": "integer"
+ },
+ "txn": {
+ "description": "The raw signed transaction.",
+ "properties": {},
+ "type": "object",
+ "x-algorand-format": "SignedTransaction"
+ }
+ },
+ "required": [
+ "pool-error",
+ "txn"
+ ],
+ "type": "object"
+ }
}
},
"description": "Given a transaction id of a recently submitted transaction, it returns information about it. There are several cases when this might succeed:\n- transaction committed (committed round > 0)\n- transaction still in the pool (committed round = 0, pool error = \"\")\n- transaction removed from pool due to error (committed round = 0, pool error != \"\")\n\nOr the transaction may have happened sufficiently long ago that the node no longer remembers it, and this will return an error."
@@ -3079,6 +3184,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Bad Request"
@@ -3089,6 +3199,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Invalid API Token"
@@ -3099,6 +3214,11 @@
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
+ },
+ "application/msgpack": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
}
},
"description": "Transaction Not Found"
diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go
index b131d52da..b2c2dab12 100644
--- a/daemon/algod/api/server/v2/generated/routes.go
+++ b/daemon/algod/api/server/v2/generated/routes.go
@@ -561,162 +561,163 @@ func RegisterHandlers(router interface {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
- "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",
+ "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",
+ "hyTJJOUAAA==",
}
// GetSwagger returns the Swagger specification corresponding to the generated code
diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go
index 8102d9010..574f66b1b 100644
--- a/data/pools/transactionPool.go
+++ b/data/pools/transactionPool.go
@@ -128,6 +128,10 @@ const timeoutOnNewBlock = time.Second
// deadline before giving up.
const assemblyWaitEps = 10 * time.Millisecond
+// ErrTxPoolStaleBlockAssembly returned by AssembleBlock when requested block number is older than the current transaction pool round
+// i.e. typically it means that we're trying to make a proposal for an older round than what the ledger is currently pointing at.
+var ErrTxPoolStaleBlockAssembly = fmt.Errorf("AssembleBlock: requested block assembly specified a round that is older than current transaction pool round")
+
// Reset resets the content of the transaction pool
func (pool *TransactionPool) Reset() {
pool.pendingTxids = make(map[transactions.Txid]txPoolVerifyCacheVal)
@@ -731,7 +735,10 @@ func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Tim
if pool.assemblyResults.err != nil {
return nil, fmt.Errorf("AssemblyBlock: encountered error for round %d: %v", round, pool.assemblyResults.err)
}
- if pool.assemblyResults.blk.Block().Round() != round {
+ if pool.assemblyResults.blk.Block().Round() > round {
+ logging.Base().Infof("AssembleBlock: requested round is behind transaction pool round %d < %d", round, pool.assemblyResults.blk.Block().Round())
+ return nil, ErrTxPoolStaleBlockAssembly
+ } else if pool.assemblyResults.blk.Block().Round() != round {
return nil, fmt.Errorf("AssembleBlock: assembled block round does not match: %d != %d",
pool.assemblyResults.blk.Block().Round(), round)
}
diff --git a/debug/genconsensusconfig/main.go b/debug/genconsensusconfig/main.go
new file mode 100644
index 000000000..33cd5dd91
--- /dev/null
+++ b/debug/genconsensusconfig/main.go
@@ -0,0 +1,39 @@
+// Copyright (C) 2019-2020 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/>.
+
+// doberman will tell you when there's something wrong with the system
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/algorand/go-algorand/config"
+)
+
+func main() {
+ path, err := os.Getwd()
+ if err != nil {
+ fmt.Printf("Unable to retieve current working directory : %v", err)
+ os.Exit(1)
+ }
+ err = config.SaveConfigurableConsensus(path, config.Consensus)
+ if err != nil {
+ fmt.Printf("Unable to save file : %v", err)
+ os.Exit(1)
+ }
+ os.Exit(0)
+}
diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go
index b74fd639e..9f6748971 100644
--- a/ledger/accountdb_test.go
+++ b/ledger/accountdb_test.go
@@ -218,6 +218,16 @@ func randomAccounts(niter int, simpleAccounts bool) map[basics.Address]basics.Ac
}
func randomDeltas(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData, imbalance int64) {
+ updates, totals, imbalance, _ = randomDeltasImpl(niter, base, rewardsLevel, true, 0)
+ return
+}
+
+func randomDeltasFull(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64, lastCreatableIDIn uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData, imbalance int64, lastCreatableID uint64) {
+ updates, totals, imbalance, lastCreatableID = randomDeltasImpl(niter, base, rewardsLevel, false, lastCreatableIDIn)
+ return
+}
+
+func randomDeltasImpl(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64, simple bool, lastCreatableIDIn uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData, imbalance int64, lastCreatableID uint64) {
proto := config.Consensus[protocol.ConsensusCurrentVersion]
updates = make(map[basics.Address]accountDelta)
totals = make(map[basics.Address]basics.AccountData)
@@ -227,6 +237,23 @@ func randomDeltas(niter int, base map[basics.Address]basics.AccountData, rewards
totals[addr] = data
}
+ // if making a full delta then need to determine max asset/app id to get rid of conflicts
+ lastCreatableID = lastCreatableIDIn
+ if !simple {
+ for _, ad := range base {
+ for aid := range ad.AssetParams {
+ if uint64(aid) > lastCreatableID {
+ lastCreatableID = uint64(aid)
+ }
+ }
+ for aid := range ad.AppParams {
+ if uint64(aid) > lastCreatableID {
+ lastCreatableID = uint64(aid)
+ }
+ }
+ }
+ }
+
// Change some existing accounts
{
i := 0
@@ -240,7 +267,12 @@ func randomDeltas(niter int, base map[basics.Address]basics.AccountData, rewards
}
i++
- new := randomAccountData(rewardsLevel)
+ var new basics.AccountData
+ if simple {
+ new = randomAccountData(rewardsLevel)
+ } else {
+ new, lastCreatableID = randomFullAccountData(rewardsLevel, lastCreatableID)
+ }
updates[addr] = accountDelta{old: old, new: new}
imbalance += int64(old.WithUpdatedRewards(proto, rewardsLevel).MicroAlgos.Raw - new.MicroAlgos.Raw)
totals[addr] = new
@@ -252,7 +284,12 @@ func randomDeltas(niter int, base map[basics.Address]basics.AccountData, rewards
for i := 0; i < niter; i++ {
addr := randomAddress()
old := totals[addr]
- new := randomAccountData(rewardsLevel)
+ var new basics.AccountData
+ if simple {
+ new = randomAccountData(rewardsLevel)
+ } else {
+ new, lastCreatableID = randomFullAccountData(rewardsLevel, lastCreatableID)
+ }
updates[addr] = accountDelta{old: old, new: new}
imbalance += int64(old.WithUpdatedRewards(proto, rewardsLevel).MicroAlgos.Raw - new.MicroAlgos.Raw)
totals[addr] = new
@@ -262,7 +299,22 @@ func randomDeltas(niter int, base map[basics.Address]basics.AccountData, rewards
}
func randomDeltasBalanced(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData) {
- updates, totals, imbalance := randomDeltas(niter, base, rewardsLevel)
+ updates, totals, _ = randomDeltasBalancedImpl(niter, base, rewardsLevel, true, 0)
+ return
+}
+
+func randomDeltasBalancedFull(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64, lastCreatableIDIn uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData, lastCreatableID uint64) {
+ updates, totals, lastCreatableID = randomDeltasBalancedImpl(niter, base, rewardsLevel, false, lastCreatableIDIn)
+ return
+}
+
+func randomDeltasBalancedImpl(niter int, base map[basics.Address]basics.AccountData, rewardsLevel uint64, simple bool, lastCreatableIDIn uint64) (updates map[basics.Address]accountDelta, totals map[basics.Address]basics.AccountData, lastCreatableID uint64) {
+ var imbalance int64
+ if simple {
+ updates, totals, imbalance = randomDeltas(niter, base, rewardsLevel)
+ } else {
+ updates, totals, imbalance, lastCreatableID = randomDeltasFull(niter, base, rewardsLevel, lastCreatableIDIn)
+ }
oldPool := base[testPoolAddr]
newPool := oldPool
@@ -271,7 +323,7 @@ func randomDeltasBalanced(niter int, base map[basics.Address]basics.AccountData,
updates[testPoolAddr] = accountDelta{old: oldPool, new: newPool}
totals[testPoolAddr] = newPool
- return updates, totals
+ return updates, totals, lastCreatableID
}
func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics.Address]basics.AccountData) {
@@ -340,6 +392,59 @@ func TestAccountDBInit(t *testing.T) {
checkAccounts(t, tx, 0, accts)
}
+// creatablesFromUpdates calculates creatables from updates
+func creatablesFromUpdates(updates map[basics.Address]accountDelta, seen map[basics.CreatableIndex]bool) map[basics.CreatableIndex]modifiedCreatable {
+ creatables := make(map[basics.CreatableIndex]modifiedCreatable)
+ for addr, update := range updates {
+ // no sets in Go, so iterate over
+ for idx := range update.old.Assets {
+ if _, ok := update.new.Assets[idx]; !ok {
+ creatables[basics.CreatableIndex(idx)] = modifiedCreatable{
+ ctype: basics.AssetCreatable,
+ created: false, // exists in old, not in new => deleted
+ creator: addr,
+ }
+ }
+ }
+ for idx := range update.new.Assets {
+ if seen[basics.CreatableIndex(idx)] {
+ continue
+ }
+ if _, ok := update.old.Assets[idx]; !ok {
+ creatables[basics.CreatableIndex(idx)] = modifiedCreatable{
+ ctype: basics.AssetCreatable,
+ created: true, // exists in new, not in old => created
+ creator: addr,
+ }
+ }
+ seen[basics.CreatableIndex(idx)] = true
+ }
+ for idx := range update.old.AppParams {
+ if _, ok := update.new.AppParams[idx]; !ok {
+ creatables[basics.CreatableIndex(idx)] = modifiedCreatable{
+ ctype: basics.AppCreatable,
+ created: false, // exists in old, not in new => deleted
+ creator: addr,
+ }
+ }
+ }
+ for idx := range update.new.AppParams {
+ if seen[basics.CreatableIndex(idx)] {
+ continue
+ }
+ if _, ok := update.old.AppParams[idx]; !ok {
+ creatables[basics.CreatableIndex(idx)] = modifiedCreatable{
+ ctype: basics.AppCreatable,
+ created: true, // exists in new, not in old => created
+ creator: addr,
+ }
+ }
+ seen[basics.CreatableIndex(idx)] = true
+ }
+ }
+ return creatables
+}
+
func TestAccountDBRound(t *testing.T) {
proto := config.Consensus[protocol.ConsensusCurrentVersion]
@@ -356,17 +461,149 @@ func TestAccountDBRound(t *testing.T) {
require.NoError(t, err)
checkAccounts(t, tx, 0, accts)
+ // used to determine how many creatables element will be in the test per iteration
+ numElementsPerSegement := 10
+
+ // lastCreatableID stores asset or app max used index to get rid of conflicts
+ lastCreatableID := crypto.RandUint64() % 512
+ ctbsList, randomCtbs := randomCreatables(numElementsPerSegement)
+ expectedDbImage := make(map[basics.CreatableIndex]modifiedCreatable)
for i := 1; i < 10; i++ {
- updates, newaccts, _ := randomDeltas(20, accts, 0)
+ var updates map[basics.Address]accountDelta
+ var newaccts map[basics.Address]basics.AccountData
+ updates, newaccts, _, lastCreatableID = randomDeltasFull(20, accts, 0, lastCreatableID)
accts = newaccts
- err = accountsNewRound(tx, updates, nil)
+ ctbsWithDeletes := randomCreatableSampling(i, ctbsList, randomCtbs,
+ expectedDbImage, numElementsPerSegement)
+ err = accountsNewRound(tx, updates, ctbsWithDeletes)
require.NoError(t, err)
- err = totalsNewRounds(tx, []map[basics.Address]accountDelta{updates}, []AccountTotals{AccountTotals{}}, []config.ConsensusParams{proto})
+ err = totalsNewRounds(tx, []map[basics.Address]accountDelta{updates}, []AccountTotals{{}}, []config.ConsensusParams{proto})
require.NoError(t, err)
err = updateAccountsRound(tx, basics.Round(i), 0)
require.NoError(t, err)
checkAccounts(t, tx, basics.Round(i), accts)
+ checkCreatables(t, tx, i, expectedDbImage)
+ }
+}
+
+// checkCreatables compares the expected database image to the actual databse content
+func checkCreatables(t *testing.T,
+ tx *sql.Tx, iteration int,
+ expectedDbImage map[basics.CreatableIndex]modifiedCreatable) {
+
+ stmt, err := tx.Prepare("SELECT asset, creator, ctype FROM assetcreators")
+ require.NoError(t, err)
+
+ defer stmt.Close()
+ rows, err := stmt.Query()
+ if err != sql.ErrNoRows {
+ require.NoError(t, err)
+ }
+ defer rows.Close()
+ counter := 0
+ for rows.Next() {
+ counter++
+ mc := modifiedCreatable{}
+ var buf []byte
+ var asset basics.CreatableIndex
+ err := rows.Scan(&asset, &buf, &mc.ctype)
+ require.NoError(t, err)
+ copy(mc.creator[:], buf)
+
+ require.NotNil(t, expectedDbImage[asset])
+ require.Equal(t, expectedDbImage[asset].creator, mc.creator)
+ require.Equal(t, expectedDbImage[asset].ctype, mc.ctype)
+ require.True(t, expectedDbImage[asset].created)
+ }
+ require.Equal(t, len(expectedDbImage), counter)
+}
+
+// randomCreatableSampling sets elements to delete from previous iteration
+// It consideres 10 elements in an iteration.
+// loop 0: returns the first 10 elements
+// loop 1: returns: * the second 10 elements
+// * random sample of elements from the first 10: created changed from true -> false
+// loop 2: returns: * the elements 20->30
+// * random sample of elements from 10->20: created changed from true -> false
+func randomCreatableSampling(iteration int, crtbsList []basics.CreatableIndex,
+ creatables map[basics.CreatableIndex]modifiedCreatable,
+ expectedDbImage map[basics.CreatableIndex]modifiedCreatable,
+ numElementsPerSegement int) map[basics.CreatableIndex]modifiedCreatable {
+
+ iteration-- // 0-based here
+
+ delSegmentEnd := iteration * numElementsPerSegement
+ delSegmentStart := delSegmentEnd - numElementsPerSegement
+ if delSegmentStart < 0 {
+ delSegmentStart = 0
+ }
+
+ newSample := make(map[basics.CreatableIndex]modifiedCreatable)
+ stop := delSegmentEnd + numElementsPerSegement
+
+ for i := delSegmentStart; i < delSegmentEnd; i++ {
+ ctb := creatables[crtbsList[i]]
+ if ctb.created && 1 == (crypto.RandUint64()%2) {
+ ctb.created = false
+ newSample[crtbsList[i]] = ctb
+ delete(expectedDbImage, crtbsList[i])
+ }
+ }
+
+ for i := delSegmentEnd; i < stop; i++ {
+ newSample[crtbsList[i]] = creatables[crtbsList[i]]
+ if creatables[crtbsList[i]].created {
+ expectedDbImage[crtbsList[i]] = creatables[crtbsList[i]]
+ }
+ }
+
+ return newSample
+}
+
+func randomCreatables(numElementsPerSegement int) ([]basics.CreatableIndex,
+ map[basics.CreatableIndex]modifiedCreatable) {
+ creatables := make(map[basics.CreatableIndex]modifiedCreatable)
+ creatablesList := make([]basics.CreatableIndex, numElementsPerSegement*10)
+ uniqueAssetIds := make(map[basics.CreatableIndex]bool)
+
+ for i := 0; i < numElementsPerSegement*10; i++ {
+ assetIndex, mc := randomCreatable(uniqueAssetIds)
+ creatables[assetIndex] = mc
+ creatablesList[i] = assetIndex
+ }
+ return creatablesList, creatables // creatablesList is needed for maintaining the order
+}
+
+// randomCreatable generates a random creatable.
+func randomCreatable(uniqueAssetIds map[basics.CreatableIndex]bool) (
+ assetIndex basics.CreatableIndex, mc modifiedCreatable) {
+
+ var ctype basics.CreatableType
+
+ switch crypto.RandUint64() % 2 {
+ case 0:
+ ctype = basics.AssetCreatable
+ case 1:
+ ctype = basics.AppCreatable
+ }
+
+ creatable := modifiedCreatable{
+ ctype: ctype,
+ created: (crypto.RandUint64() % 2) == 1,
+ creator: randomAddress(),
+ ndeltas: 1,
+ }
+
+ var assetIdx basics.CreatableIndex
+ for {
+ assetIdx = basics.CreatableIndex(crypto.RandUint64() % (uint64(2) << 50))
+ _, found := uniqueAssetIds[assetIdx]
+ if !found {
+ uniqueAssetIds[assetIdx] = true
+ break
+ }
}
+ return assetIdx, creatable
}
func BenchmarkReadingAllBalances(b *testing.B) {
diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go
index cd8122c05..a63f0458b 100644
--- a/ledger/acctupdates_test.go
+++ b/ledger/acctupdates_test.go
@@ -273,11 +273,15 @@ func TestAcctUpdates(t *testing.T) {
checkAcctUpdates(t, au, 0, 9, accts, rewardsLevels, proto)
+ // lastCreatableID stores asset or app max used index to get rid of conflicts
+ lastCreatableID := crypto.RandUint64() % 512
+ knownCreatables := make(map[basics.CreatableIndex]bool)
for i := basics.Round(10); i < basics.Round(proto.MaxBalLookback+15); i++ {
rewardLevelDelta := crypto.RandUint64() % 5
rewardLevel += rewardLevelDelta
- updates, totals := randomDeltasBalanced(1, accts[i-1], rewardLevel)
-
+ var updates map[basics.Address]accountDelta
+ var totals map[basics.Address]basics.AccountData
+ updates, totals, lastCreatableID = randomDeltasBalancedFull(1, accts[i-1], rewardLevel, lastCreatableID)
prevTotals, err := au.Totals(basics.Round(i - 1))
require.NoError(t, err)
@@ -296,8 +300,9 @@ func TestAcctUpdates(t *testing.T) {
blk.CurrentProtocol = protocol.ConsensusCurrentVersion
au.newBlock(blk, StateDelta{
- accts: updates,
- hdr: &blk.BlockHeader,
+ accts: updates,
+ hdr: &blk.BlockHeader,
+ creatables: creatablesFromUpdates(updates, knownCreatables),
})
accts = append(accts, totals)
rewardsLevels = append(rewardsLevels, rewardLevel)
diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go
index 92e4c41e4..1eb3070cd 100644
--- a/ledger/ledger_test.go
+++ b/ledger/ledger_test.go
@@ -539,6 +539,164 @@ func TestLedgerSingleTx(t *testing.T) {
a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctKeyreg, ad), "added duplicate tx")
}
+func TestLedgerSingleTxV24(t *testing.T) {
+ a := require.New(t)
+
+ backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil)
+ defer backlogPool.Shutdown()
+
+ protoName := protocol.ConsensusV24
+ genesisInitState, initSecrets := testGenerateInitState(t, protoName)
+ const inMem = true
+ log := logging.TestingLog(t)
+ cfg := config.GetDefaultLocal()
+ cfg.Archival = true
+ l, err := OpenLedger(log, t.Name(), inMem, genesisInitState, cfg)
+ a.NoError(err, "could not open ledger")
+ defer l.Close()
+
+ proto := config.Consensus[protoName]
+ poolAddr := testPoolAddr
+ sinkAddr := testSinkAddr
+
+ initAccounts := genesisInitState.Accounts
+ var addrList []basics.Address
+ for addr := range initAccounts {
+ if addr != poolAddr && addr != sinkAddr {
+ addrList = append(addrList, addr)
+ }
+ }
+
+ correctTxHeader := transactions.Header{
+ Sender: addrList[0],
+ Fee: basics.MicroAlgos{Raw: proto.MinTxnFee * 2},
+ FirstValid: l.Latest() + 1,
+ LastValid: l.Latest() + 10,
+ GenesisID: t.Name(),
+ GenesisHash: genesisInitState.GenesisHash,
+ }
+
+ assetParam := basics.AssetParams{
+ Total: 100,
+ UnitName: "unit",
+ Manager: addrList[0],
+ }
+ correctAssetConfigFields := transactions.AssetConfigTxnFields{
+ AssetParams: assetParam,
+ }
+ correctAssetConfig := transactions.Transaction{
+ Type: protocol.AssetConfigTx,
+ Header: correctTxHeader,
+ AssetConfigTxnFields: correctAssetConfigFields,
+ }
+ correctAssetTransferFields := transactions.AssetTransferTxnFields{
+ AssetAmount: 10,
+ AssetReceiver: addrList[1],
+ }
+ correctAssetTransfer := transactions.Transaction{
+ Type: protocol.AssetTransferTx,
+ Header: correctTxHeader,
+ AssetTransferTxnFields: correctAssetTransferFields,
+ }
+
+ approvalProgram := []byte("\x02\x20\x01\x01\x22") // int 1
+ clearStateProgram := []byte("\x02") // empty
+ correctAppCreateFields := transactions.ApplicationCallTxnFields{
+ ApprovalProgram: approvalProgram,
+ ClearStateProgram: clearStateProgram,
+ }
+ correctAppCreate := transactions.Transaction{
+ Type: protocol.ApplicationCallTx,
+ Header: correctTxHeader,
+ ApplicationCallTxnFields: correctAppCreateFields,
+ }
+
+ correctAppCallFields := transactions.ApplicationCallTxnFields{
+ OnCompletion: 0,
+ }
+ correctAppCall := transactions.Transaction{
+ Type: protocol.ApplicationCallTx,
+ Header: correctTxHeader,
+ ApplicationCallTxnFields: correctAppCallFields,
+ }
+
+ var badTx transactions.Transaction
+ var ad transactions.ApplyData
+
+ var assetIdx basics.AssetIndex
+ var appIdx basics.AppIndex
+
+ a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctAssetConfig, ad))
+ assetIdx = 1 // the first txn
+
+ badTx = correctAssetConfig
+ badTx.ConfigAsset = 2
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "asset 2 does not exist or has been deleted")
+
+ badTx = correctAssetConfig
+ badTx.ConfigAsset = assetIdx
+ badTx.AssetFrozen = true
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "type acfg has non-zero fields for type afrz")
+
+ badTx = correctAssetConfig
+ badTx.ConfigAsset = assetIdx
+ badTx.Sender = addrList[1]
+ badTx.AssetParams.Freeze = addrList[0]
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "this transaction should be issued by the manager")
+
+ badTx = correctAssetConfig
+ badTx.AssetParams.UnitName = "very long unit name that exceeds the limit"
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "transaction asset unit name too big: 42 > 8")
+
+ badTx = correctAssetTransfer
+ badTx.XferAsset = assetIdx
+ badTx.AssetAmount = 101
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "underflow on subtracting 101 from sender amount 100")
+
+ badTx = correctAssetTransfer
+ badTx.XferAsset = assetIdx
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), fmt.Sprintf("asset %d missing from", assetIdx))
+
+ a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctAppCreate, ad))
+ appIdx = 2 // the first txn
+
+ badTx = correctAppCreate
+ program := make([]byte, len(approvalProgram))
+ copy(program, approvalProgram)
+ program[0] = '\x01'
+ badTx.ApprovalProgram = program
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "program version must be >= 2")
+
+ badTx = correctAppCreate
+ badTx.ApplicationID = appIdx
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "programs may only be specified during application creation or update")
+
+ badTx = correctAppCall
+ badTx.ApplicationID = 0
+ err = l.appendUnvalidatedTx(t, initAccounts, initSecrets, badTx, ad)
+ a.Error(err)
+ a.Contains(err.Error(), "ApprovalProgram: invalid version")
+
+ correctAppCall.ApplicationID = appIdx
+ a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctAppCall, ad))
+}
+
func testLedgerSingleTxApplyData(t *testing.T, version protocol.ConsensusVersion) {
a := require.New(t)
diff --git a/node/node.go b/node/node.go
index 8e1ef79ec..91bdab6f1 100644
--- a/node/node.go
+++ b/node/node.go
@@ -979,6 +979,21 @@ func (vb validatedBlock) Block() bookkeeping.Block {
func (node *AlgorandFullNode) AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) {
lvb, err := node.transactionPool.AssembleBlock(round, deadline)
if err != nil {
+ if err == pools.ErrTxPoolStaleBlockAssembly {
+ // convert specific error to one that would have special handling in the agreement code.
+ err = agreement.ErrAssembleBlockRoundStale
+
+ ledgerNextRound := node.ledger.NextRound()
+ if ledgerNextRound == round {
+ // we've asked for the right round.. and the ledger doesn't think it's stale.
+ node.log.Errorf("AlgorandFullNode.AssembleBlock: could not generate a proposal for round %d, ledger and proposal generation are synced: %v", round, err)
+ } else if ledgerNextRound < round {
+ // from some reason, the ledger is behind the round that we're asking. That shouldn't happen, but error if it does.
+ node.log.Errorf("AlgorandFullNode.AssembleBlock: could not generate a proposal for round %d, ledger next round is %d: %v", round, ledgerNextRound, err)
+ }
+ // the case where ledgerNextRound > round was not implemented here on purpose. This is the "normal case" where the
+ // ledger was advancing faster then the agreement by the catchup.
+ }
return nil, err
}
return validatedBlock{vb: lvb}, nil
diff --git a/scripts/release/build/build_algod_docker.sh b/scripts/release/build/build_algod_docker.sh
index 464e6f565..1bdef00d8 100755
--- a/scripts/release/build/build_algod_docker.sh
+++ b/scripts/release/build/build_algod_docker.sh
@@ -39,11 +39,13 @@ RESULT_DIR="${HOME}/node_pkg/"
DOCKERFILE="${HOME}/go/src/github.com/algorand/go-algorand/docker/build/algod.Dockerfile"
START_ALGOD_FILE="start_algod_docker.sh"
# Use go version specified by get_golang_version.sh
-if ! GOLANG_VERSION=$("${HOME}/go/src/github.com/algorand/go-algorand/scripts/get_golang_version.sh")
+pushd "${HOME}/go/src/github.com/algorand/go-algorand"
+if ! GOLANG_VERSION=$("./scripts/get_golang_version.sh")
then
echo "${GOLANG_VERSION}"
exit 1
fi
+popd
echo "building '${DOCKERFILE}' with install file $ALGOD_INSTALL_TAR_FILE"
cp "${ALGOD_INSTALL_TAR_FILE}" "./${INPUT_ALGOD_TAR_FILE}"
diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go
index f8a1fc291..fa1ca6943 100644
--- a/test/e2e-go/upgrades/application_support_test.go
+++ b/test/e2e-go/upgrades/application_support_test.go
@@ -48,6 +48,7 @@ func makeApplicationUpgradeConsensus(t *testing.T) (appConsensus config.Consensu
// ensure it's disabled.
currentProtocolParams.Application = false
+ currentProtocolParams.SupportRekeying = false
// verify that the future protocol supports applications.
require.True(t, futureProtocolParams.Application)
diff --git a/test/e2e-go/upgrades/rekey_support_test.go b/test/e2e-go/upgrades/rekey_support_test.go
new file mode 100644
index 000000000..88d6d7c0b
--- /dev/null
+++ b/test/e2e-go/upgrades/rekey_support_test.go
@@ -0,0 +1,125 @@
+// Copyright (C) 2019-2020 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 upgrades
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/algorand/go-algorand/data/basics"
+ "github.com/algorand/go-algorand/test/framework/fixtures"
+)
+
+// TestRekeyUpgrade tests that we rekey does not work before the upgrade and works well after
+func TestRekeyUpgrade(t *testing.T) {
+ a := require.New(t)
+
+ // set the small lambda to 500 for the duration of this test.
+ roundTimeMs := 500
+ lambda := os.Getenv("ALGOSMALLLAMBDAMSEC")
+ os.Setenv("ALGOSMALLLAMBDAMSEC", fmt.Sprintf("%d", roundTimeMs))
+ defer func() {
+ if lambda == "" {
+ os.Unsetenv("ALGOSMALLLAMBDAMSEC")
+ } else {
+ os.Setenv("ALGOSMALLLAMBDAMSEC", lambda)
+ }
+ }()
+
+ consensus := makeApplicationUpgradeConsensus(t)
+
+ var fixture fixtures.RestClientFixture
+ fixture.SetConsensus(consensus)
+ fixture.Setup(t, filepath.Join("nettemplates", "TwoNodes100SecondTestUnupgradedProtocol.json"))
+ defer fixture.Shutdown()
+
+ client := fixture.GetLibGoalClientForNamedNode("Node")
+ accountList, err := fixture.GetNodeWalletsSortedByBalance(client.DataDir())
+ a.NoError(err)
+
+ accountA := accountList[0].Address
+ wh, err := client.GetUnencryptedWalletHandle()
+ a.NoError(err)
+
+ accountB, err := client.GenerateAddress(wh)
+ a.NoError(err)
+
+ round, err := client.CurrentRound()
+ a.NoError(err)
+
+ // Ensure no rekeying happened
+ ad, err := client.AccountData(accountA)
+ a.NoError(err)
+ a.Equal(basics.Address{}, ad.AuthAddr)
+
+ // rekey A -> B (RekeyTo check)
+ fee := uint64(1000)
+ amount := uint64(1000000)
+ lease := [32]byte{}
+ tx, err := client.ConstructPayment(accountA, accountB, fee, amount, nil, "", lease, basics.Round(round), basics.Round(round+1000))
+ a.NoError(err)
+
+ addrB, err := basics.UnmarshalChecksumAddress(accountB)
+ a.NoError(err)
+ tx.RekeyTo = addrB
+
+ rekey, err := client.SignTransactionWithWalletAndSigner(wh, nil, "", tx)
+ a.NoError(err)
+ _, err = client.BroadcastTransaction(rekey)
+ a.Error(err)
+ require.Contains(t, err.Error(), "transaction has RekeyTo set but rekeying not yet enable")
+
+ // use rekeyed key to authorize (AuthAddr check)
+ tx.RekeyTo = basics.Address{}
+ rekeyed, err := client.SignTransactionWithWalletAndSigner(wh, nil, accountB, tx)
+ a.NoError(err)
+ _, err = client.BroadcastTransaction(rekeyed)
+ a.Error(err)
+ require.Contains(t, err.Error(), "nonempty AuthAddr but rekeying not supported")
+ // go to upgrade
+ curStatus, err := client.Status()
+ require.NoError(t, err)
+ initialStatus := curStatus
+
+ startLoopTime := time.Now()
+
+ // wait until the network upgrade : this can take a while.
+ for curStatus.LastVersion == initialStatus.LastVersion {
+ curStatus, err = client.Status()
+ require.NoError(t, err)
+
+ require.Less(t, int64(time.Now().Sub(startLoopTime)), int64(3*time.Minute))
+ time.Sleep(time.Duration(roundTimeMs) * time.Millisecond)
+ round = curStatus.LastRound
+ }
+
+ // now, that we have upgraded to the new protocol which supports rekey, try again.
+ _, err = client.BroadcastTransaction(rekey)
+ require.NoError(t, err)
+
+ round, err = client.CurrentRound()
+ a.NoError(err)
+ client.WaitForRound(round + 1)
+
+ _, err = client.BroadcastTransaction(rekeyed)
+ require.NoError(t, err)
+}
diff --git a/test/scripts/e2e_subs/rekey.sh b/test/scripts/e2e_subs/rekey.sh
index 24fd9494d..a87c64ee4 100755
--- a/test/scripts/e2e_subs/rekey.sh
+++ b/test/scripts/e2e_subs/rekey.sh
@@ -2,9 +2,7 @@
date '+e2e_subs/rekey.sh start %Y%m%d_%H%M%S'
-set -e
-set -x
-set -o pipefail
+set -exo pipefail
export SHELLOPTS
WALLET=$1
@@ -18,33 +16,33 @@ ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }')
# Make v1 program
printf 'int 1' > "${TEMPDIR}/simplev1.teal"
-ESCROWV1=$(${gcmd} clerk compile ${TEMPDIR}/simplev1.teal -o ${TEMPDIR}/simplev1.tealc | awk '{ print $2 }')
+ESCROWV1=$(${gcmd} clerk compile "${TEMPDIR}/simplev1.teal" -o "${TEMPDIR}/simplev1.tealc" | awk '{ print $2 }')
# Make a > v1 program
printf '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal"
-ESCROWV2=$(${gcmd} clerk compile ${TEMPDIR}/simple.teal -o ${TEMPDIR}/simple.tealc | awk '{ print $2 }')
+ESCROWV2=$(${gcmd} clerk compile "${TEMPDIR}/simple.teal" -o "${TEMPDIR}/simple.tealc" | awk '{ print $2 }')
# Fund v1 escrow, v2 escrow, and ACCOUNTD
ACCOUNTD=$(${gcmd} account new|awk '{ print $6 }')
-${gcmd} clerk send -a 10000000 -f ${ACCOUNT} -t ${ESCROWV1}
-${gcmd} clerk send -a 10000000 -f ${ACCOUNT} -t ${ESCROWV2}
-${gcmd} clerk send -a 10000000 -f ${ACCOUNT} -t ${ACCOUNTD}
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ESCROWV1}"
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ESCROWV2}"
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ACCOUNTD}"
# Plan: make a txn group. First one is rekey-to payment from $ACCOUNTD, second
# one is regular payment from v1 escrow. (Should fail when we send it).
-${gcmd} clerk send -a 1 -f ${ACCOUNTD} -t ${ACCOUNTD} --rekey-to ${ACCOUNT} -o ${TEMPDIR}/txn0.tx
-${gcmd} clerk send -a 1 --from-program "${TEMPDIR}/simplev1.teal" -t ${ACCOUNTD} -o ${TEMPDIR}/txn1.tx
-cat ${TEMPDIR}/txn0.tx ${TEMPDIR}/txn1.tx > ${TEMPDIR}/group0.tx
+${gcmd} clerk send -a 1 -f "${ACCOUNTD}" -t "${ACCOUNTD}" --rekey-to "${ACCOUNT}" -o "${TEMPDIR}/txn0.tx"
+${gcmd} clerk send -a 1 --from-program "${TEMPDIR}/simplev1.teal" -t "${ACCOUNTD}" -o "${TEMPDIR}/txn1.tx"
+cat "${TEMPDIR}/txn0.tx" "${TEMPDIR}/txn1.tx" > "${TEMPDIR}/group0.tx"
# Build + sign group
-${gcmd} clerk group -i ${TEMPDIR}/group0.tx -o ${TEMPDIR}/group0_grouped.tx
-${gcmd} clerk split -i ${TEMPDIR}/group0_grouped.tx -o ${TEMPDIR}/group0_split.txn
-${gcmd} clerk sign -i ${TEMPDIR}/group0_split-0.txn -o ${TEMPDIR}/group0_split-0.stxn
-cat ${TEMPDIR}/group0_split-0.stxn ${TEMPDIR}/group0_split-1.txn > ${TEMPDIR}/group0_signed.stxn
+${gcmd} clerk group -i "${TEMPDIR}/group0.tx" -o "${TEMPDIR}/group0_grouped.tx"
+${gcmd} clerk split -i "${TEMPDIR}/group0_grouped.tx" -o "${TEMPDIR}/group0_split.txn"
+${gcmd} clerk sign -i "${TEMPDIR}/group0_split-0.txn" -o "${TEMPDIR}/group0_split-0.stxn"
+cat "${TEMPDIR}/group0_split-0.stxn" "${TEMPDIR}/group0_split-1.txn" > "${TEMPDIR}/group0_signed.stxn"
# Broadcast group (should fail)
-RES=$(${gcmd} clerk rawsend -f ${TEMPDIR}/group0_signed.stxn 2>&1 || true)
+RES=$(${gcmd} clerk rawsend -f "${TEMPDIR}/group0_signed.stxn" 2>&1 || true)
EXPERROR='program version must be >= 2 for this transaction group'
if [[ $RES != *"${EXPERROR}"* ]]; then
date '+e2e_subs/rekey.sh FAIL txn group with rekey transaction should require teal version >= 2 %Y%m%d_%H%M%S'
@@ -54,35 +52,73 @@ fi
# Plan: make a txn group. First one is rekey-to payment from $ACCOUNTD, second
# one is regular payment from v1 escrow. (Should succeed when we send it).
-${gcmd} clerk send -a 1 -f ${ACCOUNTD} -t ${ACCOUNTD} --rekey-to ${ACCOUNT} -o ${TEMPDIR}/txn2.tx
-${gcmd} clerk send -a 1 --from-program "${TEMPDIR}/simple.teal" -t ${ACCOUNTD} -o ${TEMPDIR}/txn3.tx
-cat ${TEMPDIR}/txn2.tx ${TEMPDIR}/txn3.tx > ${TEMPDIR}/group1.tx
+${gcmd} clerk send -a 1 -f "${ACCOUNTD}" -t "${ACCOUNTD}" --rekey-to "${ACCOUNT}" -o "${TEMPDIR}/txn2.tx"
+${gcmd} clerk send -a 1 --from-program "${TEMPDIR}/simple.teal" -t "${ACCOUNTD}" -o "${TEMPDIR}/txn3.tx"
+cat "${TEMPDIR}/txn2.tx" "${TEMPDIR}/txn3.tx" > "${TEMPDIR}/group1.tx"
# Build + sign group
-${gcmd} clerk group -i ${TEMPDIR}/group1.tx -o ${TEMPDIR}/group1_grouped.tx
-${gcmd} clerk split -i ${TEMPDIR}/group1_grouped.tx -o ${TEMPDIR}/group1_split.txn
-${gcmd} clerk sign -i ${TEMPDIR}/group1_split-0.txn -o ${TEMPDIR}/group1_split-0.stxn
-cat ${TEMPDIR}/group1_split-0.stxn ${TEMPDIR}/group1_split-1.txn > ${TEMPDIR}/group1_signed.stxn
+${gcmd} clerk group -i "${TEMPDIR}/group1.tx" -o "${TEMPDIR}/group1_grouped.tx"
+${gcmd} clerk split -i "${TEMPDIR}/group1_grouped.tx" -o "${TEMPDIR}/group1_split.txn"
+${gcmd} clerk sign -i "${TEMPDIR}/group1_split-0.txn" -o "${TEMPDIR}/group1_split-0.stxn"
+cat "${TEMPDIR}/group1_split-0.stxn" "${TEMPDIR}/group1_split-1.txn" > "${TEMPDIR}/group1_signed.stxn"
# Broadcast group (should succeed)
-${gcmd} clerk rawsend -f ${TEMPDIR}/group1_signed.stxn
+${gcmd} clerk rawsend -f "${TEMPDIR}/group1_signed.stxn"
# Regular rekeying test
-algokey generate > ${TEMPDIR}/rekey
-mnemonic=$(grep 'Private key mnemonic:' < ${TEMPDIR}/rekey | sed 's/Private key mnemonic: //')
-ACCOUNTC=$(grep 'Public key:' < ${TEMPDIR}/rekey | sed 's/Public key: //')
+algokey generate > "${TEMPDIR}/rekey"
+mnemonic=$(grep 'Private key mnemonic:' < "${TEMPDIR}/rekey" | sed 's/Private key mnemonic: //')
+ACCOUNTC=$(grep 'Public key:' < "${TEMPDIR}/rekey" | sed 's/Public key: //')
${gcmd} account import -m "${mnemonic}"
-${gcmd} clerk send -a 10000000 -f ${ACCOUNT} -t ${ACCOUNTB} --rekey-to ${ACCOUNTC}
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ACCOUNTB}" --rekey-to "${ACCOUNTC}"
-${gcmd} clerk send -a 13000000 -f ${ACCOUNT} -t ${ACCOUNTB} -o ${TEMPDIR}/ntxn
-${gcmd} clerk sign -S ${ACCOUNTC} -i ${TEMPDIR}/ntxn -o ${TEMPDIR}/nstxn
-${gcmd} clerk rawsend -f ${TEMPDIR}/nstxn
+${gcmd} clerk send -a 13000000 -f "${ACCOUNT}" -t "${ACCOUNTB}" -o "${TEMPDIR}/ntxn"
+${gcmd} clerk sign -S "${ACCOUNTC}" -i "${TEMPDIR}/ntxn" -o "${TEMPDIR}/nstxn"
+${gcmd} clerk rawsend -f "${TEMPDIR}/nstxn"
-BALANCEB=$(${gcmd} account balance -a ${ACCOUNTB} | awk '{ print $1 }')
-if [ $BALANCEB -ne 23000000 ]; then
- date '+e2e_subs/rekey.sh FAIL wanted balance=23000000 but got ${BALANCEB} %Y%m%d_%H%M%S'
+BALANCEB=$(${gcmd} account balance -a "${ACCOUNTB}" | awk '{ print $1 }')
+if [ "$BALANCEB" -ne 23000000 ]; then
+ date "+e2e_subs/rekey.sh FAIL wanted balance=23000000 but got ${BALANCEB} %Y%m%d_%H%M%S"
+ false
+fi
+
+# Rekey from A to C back to A [A -> C -> A].
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ACCOUNTB}" --rekey-to "${ACCOUNT}" -s -o "${TEMPDIR}/ntxn2"
+${gcmd} clerk sign -S "${ACCOUNTC}" -i "${TEMPDIR}/ntxn2" -o "${TEMPDIR}/nstxn2"
+${gcmd} clerk rawsend -f "${TEMPDIR}/nstxn2"
+
+BALANCEB=$(${gcmd} account balance -a "${ACCOUNTB}" | awk '{ print $1 }')
+if [ "$BALANCEB" -ne 33000000 ]; then
+ date "+e2e_subs/rekey.sh FAIL wanted balance=33000000 but got ${BALANCEB} %Y%m%d_%H%M%S"
+ false
+fi
+
+# Fail case. Try to sign and send from A signed by C.
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ACCOUNTB}" -s -o "${TEMPDIR}/ntxn3"
+${gcmd} clerk sign -S "${ACCOUNTC}" -i "${TEMPDIR}/ntxn3" -o "${TEMPDIR}/nstxn3"
+
+# This should fail because $ACCOUNT should have signed the transaction.
+if ! ${gcmd} clerk rawsend -f "${TEMPDIR}/nstxn3"; then
+ date '+e2e_subs/rekey.sh OK %Y%m%d_%H%M%S'
+else
+ date "+e2e_subs/rekey.sh rawsend should have failed because of a bad signature %Y%m%d_%H%M%S"
+ false
+fi
+
+# Account balance should be the same amount as before.
+BALANCEB=$(${gcmd} account balance -a "${ACCOUNTB}" | awk '{ print $1 }')
+if [ "$BALANCEB" -ne 33000000 ]; then
+ date "+e2e_subs/rekey.sh FAIL wanted balance=33000000 but got ${BALANCEB} %Y%m%d_%H%M%S"
+ false
+fi
+
+# After restoring, let's just do a trivial transfer as a sanity.
+${gcmd} clerk send -a 10000000 -f "${ACCOUNT}" -t "${ACCOUNTB}"
+
+BALANCEB=$(${gcmd} account balance -a "${ACCOUNTB}" | awk '{ print $1 }')
+if [ "$BALANCEB" -ne 43000000 ]; then
+ date "+e2e_subs/rekey.sh FAIL wanted balance=43000000 but got ${BALANCEB} %Y%m%d_%H%M%S"
false
fi
-date '+e2e_subs/rekey.sh OK %Y%m%d_%H%M%S'