From d48da3322a447ab40bb08028c96a366e586d64ca Mon Sep 17 00:00:00 2001 From: John Lee Date: Mon, 1 Jun 2020 18:26:12 -0400 Subject: [PATCH 001/267] Update AMI for arm buildhost (#506) (#1117) The AMI for the ARM host builder was updated in rel/beta, but not in master. This fixes that. --- scripts/buildhost/linux-arm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/buildhost/linux-arm.sh b/scripts/buildhost/linux-arm.sh index 70f5dadb3d..d65770f419 100755 --- a/scripts/buildhost/linux-arm.sh +++ b/scripts/buildhost/linux-arm.sh @@ -19,7 +19,7 @@ cd ${TMPPATH} AWS_REGION="us-west-2" # this is the private AMI that contains the RasPI VM running on port 5022 -AWS_LINUX_AMI="ami-092a4cbb66cbd47f7" +AWS_LINUX_AMI="ami-06819013739d79715" AWS_INSTANCE_TYPE="i3.xlarge" INSTANCE_NUMBER=$RANDOM From 9e491f7c546258e5fde62c5450475e659849fb32 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 1 Jun 2020 22:06:38 -0400 Subject: [PATCH 002/267] Avoid regenerating msgp decoders/encoders dynamically on arm (#1119) Summary Compiling on ARM takes a long time. As a partial workaround, we want to eliminate recreating the message-pack encoders/decoders, as these already generated by other platforms. --- scripts/travis/build.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index a80667bf42..ddf54989cb 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -58,17 +58,19 @@ scripts/travis/before_build.sh # Force re-evaluation of genesis files to see if source files changed w/o running make touch gen/generate.go -# Force re-generation of msgpack encoders/decoders with msgp. If this re-generated code -# does not match the checked-in code, some structs may have been added or updated without -# refreshing the generated codecs. The enlistment check below will error out, if so. -make msgp - if [ "${OS}-${ARCH}" = "linux-arm" ]; then # for arm, build just the basic distro MAKE_DEBUG_OPTION="" fi if [ "${MAKE_DEBUG_OPTION}" != "" ]; then + # Force re-generation of msgpack encoders/decoders with msgp. If this re-generated code + # does not match the checked-in code, some structs may have been added or updated without + # refreshing the generated codecs. The enlistment check below will error out, if so. + # we want to have that only on system where we have some debugging abilities. Platforms that do not support + # debugging ( i.e. arm ) are also usually under powered and making this extra step + # would be very costly there. + make msgp make build build-race else make build From a81c57815f8cb68b34e0f68cf730c10571f6cd20 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 1 Jun 2020 22:57:02 -0400 Subject: [PATCH 003/267] Fix datarace when writing a catchpoint label (#1120) Summary This PR address a potential data race detected by our automation testing. The data race occurs when a catchpoint label is being written to disk and a new block is added to the ledger at the same time. Test Plan Repurpose one of the accountUpdates unit test to verify the fix works as expected. --- ledger/acctupdates.go | 47 ++++++++++++++-------- ledger/acctupdates_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 770553fe81..18685761a8 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -900,28 +900,18 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Addre return } -func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound, balancesRound basics.Round, ledgerBlockDigest crypto.Digest) (label string, err error) { - var totals AccountTotals +func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Round, totals AccountTotals, ledgerBlockDigest crypto.Digest) (label string, err error) { var balancesHash crypto.Digest - var offset uint64 - - offset, err = au.roundOffset(balancesRound) - if err != nil { - return - } - totals = au.roundTotals[offset] balancesHash, err = au.balancesTrie.RootHash() if err != nil { return } + cpLabel := makeCatchpointLabel(committedRound, ledgerBlockDigest, balancesHash, totals) label = cpLabel.String() _, err = au.accountsq.writeCatchpointStateString(context.Background(), catchpointStateLastCatchpoint, label) - if err == nil { - au.lastCatchpointLabel = label - } return } @@ -968,7 +958,22 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb defer au.accountsWriting.Done() au.accountsMu.RLock() - // adjust teh offset according to what happend meanwhile.. + // we can exit right away, as this is the result of mis-ordered call to committedUpTo. + if au.dbRound < dbRound || offset < uint64(au.dbRound-dbRound) { + // if this is an archival ledger, we might need to close the catchpointWriting channel + if au.archivalLedger { + // determine if this was a catchpoint round + isCatchpointRound := ((offset + uint64(lookback+dbRound)) > 0) && (au.catchpointInterval != 0) && (0 == (uint64((offset + uint64(lookback+dbRound))) % au.catchpointInterval)) + if isCatchpointRound { + // it was a catchpoint round, so close the channel. + close(au.catchpointWriting) + } + } + au.accountsMu.RUnlock() + return + } + + // adjust the offset according to what happend meanwhile.. offset -= uint64(au.dbRound - dbRound) dbRound = au.dbRound @@ -976,7 +981,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb flushTime := time.Now() isCatchpointRound := ((offset + uint64(lookback+dbRound)) > 0) && (au.catchpointInterval != 0) && (0 == (uint64((offset + uint64(lookback+dbRound))) % au.catchpointInterval)) - // create a copy of thej deltas, reward levels and protos for the range we're going to flush. + // create a copy of the deltas, round totals and protos for the range we're going to flush. deltas := make([]map[basics.Address]accountDelta, offset, offset) roundTotals := make([]AccountTotals, offset+1, offset+1) protos := make([]config.ConsensusParams, offset+1, offset+1) @@ -995,6 +1000,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } } + var committedRoundDigest crypto.Digest + + if isCatchpointRound { + committedRoundDigest = au.roundDigest[offset+uint64(lookback)-1] + } + au.accountsMu.RUnlock() // in committedUpTo, we expect that this function we close the catchpointWriting when @@ -1013,7 +1024,6 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } var catchpointLabel string - var committedRoundDigest crypto.Digest err := au.dbs.wdb.Atomic(func(tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) @@ -1055,8 +1065,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb return } if isCatchpointRound { - committedRoundDigest = au.roundDigest[offset+uint64(lookback)-1] - catchpointLabel, err = au.accountsCreateCatchpointLabel(dbRound+basics.Round(offset)+lookback, dbRound+basics.Round(offset), committedRoundDigest) + catchpointLabel, err = au.accountsCreateCatchpointLabel(dbRound+basics.Round(offset)+lookback, roundTotals[offset], committedRoundDigest) if err != nil { au.log.Warnf("commitRound : unable to create a catchpoint label: %v", err) } @@ -1069,6 +1078,10 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } au.accountsMu.Lock() + if isCatchpointRound && catchpointLabel != "" { + au.lastCatchpointLabel = catchpointLabel + } + // Drop reference counts to modified accounts, and evict them // from in-memory cache when no references remain. for addr, cnt := range flushcount { diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 88f758428e..894dd3d165 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -19,6 +19,7 @@ package ledger import ( "fmt" "runtime" + "sync" "testing" "time" @@ -264,6 +265,87 @@ func TestAcctUpdates(t *testing.T) { } } +func TestAcctUpdatesFastUpdates(t *testing.T) { + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + t.Skip("This test is too slow on ARM and causes travis builds to time out") + } + proto := config.Consensus[protocol.ConsensusCurrentVersion] + + ml := makeMockLedgerForTracker(t) + defer ml.close() + ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) + + accts := []map[basics.Address]basics.AccountData{randomAccounts(20)} + rewardsLevels := []uint64{0} + + pooldata := basics.AccountData{} + pooldata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + pooldata.Status = basics.NotParticipating + accts[0][testPoolAddr] = pooldata + + sinkdata := basics.AccountData{} + sinkdata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + sinkdata.Status = basics.NotParticipating + accts[0][testSinkAddr] = sinkdata + + au := &accountUpdates{} + conf := config.GetDefaultLocal() + conf.CatchpointInterval = 1 + au.initialize(conf, ".", proto, accts[0]) + defer au.close() + + err := au.loadFromDisk(ml) + require.NoError(t, err) + + // cover 10 genesis blocks + rewardLevel := uint64(0) + for i := 1; i < 10; i++ { + accts = append(accts, accts[0]) + rewardsLevels = append(rewardsLevels, rewardLevel) + } + + checkAcctUpdates(t, au, 0, 9, accts, rewardsLevels, proto) + + wg := sync.WaitGroup{} + + 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) + + prevTotals, err := au.totals(basics.Round(i - 1)) + require.NoError(t, err) + + oldPool := accts[i-1][testPoolAddr] + newPool := totals[testPoolAddr] + newPool.MicroAlgos.Raw -= prevTotals.RewardUnits() * rewardLevelDelta + updates[testPoolAddr] = accountDelta{old: oldPool, new: newPool} + totals[testPoolAddr] = newPool + + blk := bookkeeping.Block{ + BlockHeader: bookkeeping.BlockHeader{ + Round: basics.Round(i), + }, + } + blk.RewardsLevel = rewardLevel + blk.CurrentProtocol = protocol.ConsensusCurrentVersion + + au.newBlock(blk, StateDelta{ + accts: updates, + hdr: &blk.BlockHeader, + }) + accts = append(accts, totals) + rewardsLevels = append(rewardsLevels, rewardLevel) + + wg.Add(1) + go func(round basics.Round) { + defer wg.Done() + au.committedUpTo(round) + }(i) + } + wg.Wait() +} + func BenchmarkBalancesChanges(b *testing.B) { if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { b.Skip("This test is too slow on ARM and causes travis builds to time out") From b6524b69c33e6a87cd6033bd245773132b81734b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 2 Jun 2020 08:23:19 -0400 Subject: [PATCH 004/267] Fix two more dataraces during catchup mode switch. (#1121) --- ledger/acctupdates.go | 8 ++++---- node/node.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 18685761a8..2a3a473368 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -764,7 +764,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo close(au.catchpointWriting) au.ctx, au.ctxCancel = context.WithCancel(context.Background()) au.committedOffset = make(chan deferedCommit, 1) - go au.commitSyncer() + go au.commitSyncer(au.committedOffset) lastBalancesRound = au.dbRound @@ -930,10 +930,10 @@ func (au *accountUpdates) roundOffset(rnd basics.Round) (offset uint64, err erro return off, nil } -func (au *accountUpdates) commitSyncer() { +func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { for { select { - case committedOffset, ok := <-au.committedOffset: + case committedOffset, ok := <-deferedCommits: if !ok { return } @@ -943,7 +943,7 @@ func (au *accountUpdates) commitSyncer() { drained := false for !drained { select { - case <-au.committedOffset: + case <-deferedCommits: au.accountsWriting.Done() default: drained = true diff --git a/node/node.go b/node/node.go index 82d9b42441..f765e9aa09 100644 --- a/node/node.go +++ b/node/node.go @@ -910,14 +910,14 @@ func (node *AlgorandFullNode) SetCatchpointCatchupMode(catchpointCatchupMode boo node.log.Infof("Indexer is not available - %v", err) } - node.cancelCtx() + // Set up a context we can use to cancel goroutines on Stop() + node.ctx, node.cancelCtx = context.WithCancel(context.Background()) node.startMonitoringRoutines() + // at this point, the catchpoint catchup is done ( either successfully or not.. ) node.catchpointCatchupService = nil - // Set up a context we can use to cancel goroutines on Stop() - node.ctx, node.cancelCtx = context.WithCancel(context.Background()) return node.ctx } From 9b8b0bf3a5062a59b0224928a71760cd4377a103 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 08:32:43 -0400 Subject: [PATCH 005/267] heavily pared down checkin to figure out compilation issues --- daemon/algod/api/server/v2/handlers.go | 2 +- .../algod/api/server/v2/test/handlers_test.go | 18 ++ daemon/algod/api/server/v2/test/helpers.go | 230 ++++++++++++++++++ node/node.go | 26 ++ 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 daemon/algod/api/server/v2/test/handlers_test.go create mode 100644 daemon/algod/api/server/v2/test/helpers.go diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 410040cd27..6fc13b8b31 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -46,7 +46,7 @@ const maxTealSourceBytes = 1e5 // Handlers is an implementation to the V2 route handler interface defined by the generated code. type Handlers struct { - Node *node.AlgorandFullNode + Node *node.NodeInterface Log logging.Logger Shutdown <-chan struct{} } diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go new file mode 100644 index 0000000000..00adc4d6b6 --- /dev/null +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -0,0 +1,18 @@ +package test + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSimple(t *testing.T) { + numAccounts := 10 + numTransactions := 10 + offlineAccounts := true + _, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + //mockNode := makeMockNode(mockLedger, t.Name()) + + require.NoError(t, nil) +} diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go new file mode 100644 index 0000000000..a4c2b8082a --- /dev/null +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -0,0 +1,230 @@ +package test + +import ( + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data" + "github.com/algorand/go-algorand/data/account" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/node" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" + "math/rand" + "strconv" + "testing" +) + +type mockNode struct { + ledger *data.Ledger + genesisID string +} + +func makeMockNode(ledger *data.Ledger, genesisID string) mockNode { + return mockNode{ledger: ledger, genesisID: genesisID} +} + +func (m mockNode) Ledger() *data.Ledger { + return m.ledger +} + +func (m mockNode) Status() (s node.StatusReport, err error) { + s = node.StatusReport{ + LastRound: basics.Round(1), + } + return +} +func (m mockNode) GenesisID() string { + return m.genesisID +} + +func (m mockNode) GenesisHash() crypto.Digest { + return m.ledger.GenesisHash() +} + +func (m mockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { + return nil +} + +func (m mockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { + res = node.TxnWithStatus{} + found = true + return +} + +func (m mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { + stxns := make([]transactions.SignedTxn, 1) + return stxns, nil +} + +func (m mockNode) SuggestedFee() basics.MicroAlgos { + return basics.MicroAlgos{1} +} + +// unused by handlers: +//Config() config.Local +//Start() +//ListeningAddress() (string, bool) +//Stop() +//ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]TxnWithStatus, error) +//GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (TxnWithStatus, bool) +//PoolStats() PoolStats +//IsArchival() bool +//OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) +//Uint64() uint64 +//Indexer() (*indexer.Indexer, error) +//GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) +//AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) + +////// mock ledger testing environment follows + +var sinkAddr = basics.Address{0x7, 0xda, 0xcb, 0x4b, 0x6d, 0x9e, 0xd1, 0x41, 0xb1, 0x75, 0x76, 0xbd, 0x45, 0x9a, 0xe6, 0x42, 0x1d, 0x48, 0x6d, 0xa3, 0xd4, 0xef, 0x22, 0x47, 0xc4, 0x9, 0xa3, 0x96, 0xb8, 0x2e, 0xa2, 0x21} +var poolAddr = basics.Address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} +var genesisHash = crypto.Digest{0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe} +var genesisID = "testingid" + +var proto = config.Consensus[protocol.ConsensusCurrentVersion] + +func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*data.Ledger, []account.Root, []account.Participation, []transactions.SignedTxn, func()) { + P := numAccounts // n accounts + TXs := numTxs // n txns + maxMoneyAtStart := 1000000 // max money start + minMoneyAtStart := 100000 // max money start + transferredMoney := 100 // max money/txn + maxFee := 10 // max maxFee/txn + lastValid := basics.Round(500) // max round + + accessors := []db.Accessor{} + + release := func() { + for _, acc := range accessors { + acc.Close() + } + } + + // generate accounts + genesis := make(map[basics.Address]basics.AccountData) + gen := rand.New(rand.NewSource(2)) + roots := make([]account.Root, P) + parts := make([]account.Participation, P) + for i := 0; i < P; i++ { + access, err := db.MakeAccessor(t.Name()+"_root_testingenv"+strconv.Itoa(i), false, true) + if err != nil { + panic(err) + } + accessors = append(accessors, access) + + root, err := account.GenerateRoot(access) + if err != nil { + panic(err) + } + + access, err = db.MakeAccessor(t.Name()+"_part_testingenv"+strconv.Itoa(i), false, true) + if err != nil { + panic(err) + } + accessors = append(accessors, access) + + part, err := account.FillDBWithParticipationKeys(access, root.Address(), 0, lastValid, config.Consensus[protocol.ConsensusCurrentVersion].DefaultKeyDilution) + if err != nil { + panic(err) + } + + roots[i] = root + parts[i] = part + + startamt := basics.MicroAlgos{Raw: uint64(minMoneyAtStart + (gen.Int() % (maxMoneyAtStart - minMoneyAtStart)))} + short := root.Address() + + if offlineAccounts && i > P/2 { + genesis[short] = basics.MakeAccountData(basics.Offline, startamt) + } else { + data := basics.MakeAccountData(basics.Online, startamt) + data.SelectionID = parts[i].VRFSecrets().PK + data.VoteID = parts[i].VotingSecrets().OneTimeSignatureVerifier + genesis[short] = data + } + } + + genesis[poolAddr] = basics.MakeAccountData(basics.NotParticipating, basics.MicroAlgos{Raw: 100000 * uint64(proto.RewardsRateRefreshInterval)}) + + bootstrap := data.MakeGenesisBalances(genesis, poolAddr, sinkAddr) + + // generate test transactions + const inMem = true + const archival = true + ledger, err := data.LoadLedger(logging.Base(), t.Name(), inMem, protocol.ConsensusCurrentVersion, bootstrap, genesisID, genesisHash, nil, archival) + if err != nil { + panic(err) + } + + tx := make([]transactions.SignedTxn, TXs) + latest := ledger.Latest() + bal, err := ledger.AllBalances(latest) + if err != nil { + panic(err) + } + + for i := 0; i < TXs; i++ { + send := gen.Int() % P + recv := gen.Int() % P + + saddr := roots[send].Address() + raddr := roots[recv].Address() + + if proto.MinTxnFee+uint64(maxFee) > bal[saddr].MicroAlgos.Raw { + continue + } + + xferMax := transferredMoney + if uint64(xferMax) > bal[saddr].MicroAlgos.Raw-proto.MinTxnFee-uint64(maxFee) { + xferMax = int(bal[saddr].MicroAlgos.Raw - proto.MinTxnFee - uint64(maxFee)) + } + + if xferMax == 0 { + continue + } + + amt := basics.MicroAlgos{Raw: uint64(gen.Int() % xferMax)} + fee := basics.MicroAlgos{Raw: uint64(gen.Int()%maxFee) + proto.MinTxnFee} + + t := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: saddr, + Fee: fee, + FirstValid: ledger.LastRound(), + LastValid: ledger.LastRound() + lastValid, + Note: make([]byte, 4), + GenesisHash: genesisHash, + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: raddr, + Amount: amt, + }, + } + + rand.Read(t.Note) + //if reallySignTxs { + tx[i] = t.Sign(roots[send].Secrets()) + //} else { + // tx[i] = transactions.SignedTxn{Transaction: t, Sig: crypto.Signature{}} + //} + + sbal := bal[saddr] + sbal.MicroAlgos.Raw -= fee.Raw + sbal.MicroAlgos.Raw -= amt.Raw + bal[saddr] = sbal + + ibal := bal[poolAddr] + ibal.MicroAlgos.Raw += fee.Raw + bal[poolAddr] = ibal + + rbal := bal[raddr] + rbal.MicroAlgos.Raw += amt.Raw + bal[raddr] = rbal + } + + return ledger, roots, parts, tx, release +} diff --git a/node/node.go b/node/node.go index f765e9aa09..db26393bcf 100644 --- a/node/node.go +++ b/node/node.go @@ -83,6 +83,32 @@ func (status StatusReport) TimeSinceLastRound() time.Duration { return time.Since(status.LastRoundTimestamp) } +// NodeInterface represents node fns. +type NodeInterface interface { + Ledger() *data.Ledger // used by handlers; ledger is in turn used extensively, so that might need mocking too? + Status() (s StatusReport, err error) // used by handlers + GenesisID() string // used by handlers + GenesisHash() crypto.Digest // used by handlers + BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error // used by handlers + GetPendingTransaction(txID transactions.Txid) (res TxnWithStatus, found bool) // used by handlers + GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) // used by handlers + SuggestedFee() basics.MicroAlgos // used by handlers + // unused by handlers: + Config() config.Local + Start() + ListeningAddress() (string, bool) + Stop() + ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]TxnWithStatus, error) + GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (TxnWithStatus, bool) + PoolStats() PoolStats + IsArchival() bool + OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) + Uint64() uint64 + Indexer() (*indexer.Indexer, error) + GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) + AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) +} + // AlgorandFullNode specifies and implements a full Algorand node. type AlgorandFullNode struct { nodeContextData From 93e0bc5de1e76dd9434e3e398ca6bbf8718aaeb6 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 12:37:07 -0400 Subject: [PATCH 006/267] add StartCatchup and AbortCatchup to NodeInterface change member Node in v2 Handlers to be interface rather than pointer to interface fill in unused aspects of MockNode clean up testingenv slightly (based on data package testingenv) implement tests --- daemon/algod/api/server/v2/handlers.go | 2 +- .../algod/api/server/v2/test/handlers_test.go | 226 +++++++++++++++++- daemon/algod/api/server/v2/test/helpers.go | 93 ++++--- node/node.go | 2 + 4 files changed, 289 insertions(+), 34 deletions(-) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 6fc13b8b31..935bb8efd1 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -46,7 +46,7 @@ const maxTealSourceBytes = 1e5 // Handlers is an implementation to the V2 route handler interface defined by the generated code. type Handlers struct { - Node *node.NodeInterface + Node node.NodeInterface Log logging.Logger Shutdown <-chan struct{} } diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 00adc4d6b6..8f1a533c29 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -1,18 +1,232 @@ package test import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" "testing" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/require" + + v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" + generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" + "github.com/algorand/go-algorand/data/account" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" ) -func TestSimple(t *testing.T) { - numAccounts := 10 - numTransactions := 10 +func setupTestForMethodGet(t *testing.T) (v2.Handlers, echo.Context, *httptest.ResponseRecorder, []account.Root, []transactions.SignedTxn, func()) { + numAccounts := 1 + numTransactions := 1 offlineAccounts := true - _, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + mockLedger, rootkeys, _, stxns, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + mockNode := makeMockNode(mockLedger, t.Name()) + dummyShutdownChan := make(chan struct{}) + handler := v2.Handlers{ + Node: mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + return handler, c, rec, rootkeys, stxns, releasefunc +} + +func TestSimpleMockBuilding(t *testing.T) { + handler, _, _, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + require.Equal(t, t.Name(), handler.Node.GenesisID()) +} + +func accountInformationTest(t *testing.T, address string, expectedCode int) { + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.AccountInformation(c, address) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestAccountInformation(t *testing.T) { + accountInformationTest(t, "ALGORANDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIN5DNAU", 200) + accountInformationTest(t, "malformed", 400) +} + +func getBlockTest(t *testing.T, blockNum uint64, format string, expectedCode int) { + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.GetBlock(c, blockNum, generatedV2.GetBlockParams{Format: &format}) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestGetBlock(t *testing.T) { + getBlockTest(t, 0, "json", 200) + getBlockTest(t, 0, "msgpack", 200) + getBlockTest(t, 1, "json", 500) + getBlockTest(t, 0, "malformed", 400) +} + +func TestGetSupply(t *testing.T) { + handler, c, _, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.GetSupply(c) + require.NoError(t, err) +} + +func TestGetStatus(t *testing.T) { + handler, c, _, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.GetStatus(c) + require.NoError(t, err) +} + +func TestGetStatusAfterBlock(t *testing.T) { + t.Skip("skipping for now as this waits up to a minute") + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.WaitForBlock(c, 0) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) +} + +func TestGetTransactionParams(t *testing.T) { + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + err := handler.TransactionParams(c) + require.NoError(t, err) + require.Equal(t, 200, rec.Code) +} + +func pendingTransactionInformationTest(t *testing.T, txidToUse int, format string, expectedCode int) { + handler, c, rec, _, stxns, releasefunc := setupTestForMethodGet(t) defer releasefunc() - //mockNode := makeMockNode(mockLedger, t.Name()) + txid := "badtxid" + if txidToUse >= 0 { + txid = stxns[txidToUse].ID().String() + } + params := generatedV2.PendingTransactionInformationParams{Format: &format} + err := handler.PendingTransactionInformation(c, txid, params) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestPendingTransactionInformation(t *testing.T) { + pendingTransactionInformationTest(t, 0, "json", 200) + pendingTransactionInformationTest(t, 0, "msgpack", 200) + pendingTransactionInformationTest(t, -1, "json", 400) + pendingTransactionInformationTest(t, 0, "bad format", 400) +} + +func getPendingTransactionsTest(t *testing.T, format string, expectedCode int) { + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + params := generatedV2.GetPendingTransactionsParams{Format: &format} + err := handler.GetPendingTransactions(c, params) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestPendingTransactions(t *testing.T) { + getPendingTransactionsTest(t, "json", 200) + getPendingTransactionsTest(t, "msgpack", 200) + getPendingTransactionsTest(t, "bad format", 400) +} + +func pendingTransactionsByAddressTest(t *testing.T, rootkeyToUse int, format string, expectedCode int) { + handler, c, rec, rootkeys, _, releasefunc := setupTestForMethodGet(t) + defer releasefunc() + address := "bad address" + if rootkeyToUse >= 0 { + address = rootkeys[rootkeyToUse].Address().String() + } + params := generatedV2.GetPendingTransactionsByAddressParams{Format: &format} + err := handler.GetPendingTransactionsByAddress(c, address, params) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestPendingTransactionsByAddress(t *testing.T) { + pendingTransactionsByAddressTest(t, 0, "json", 200) + pendingTransactionsByAddressTest(t, 0, "msgpack", 200) + pendingTransactionsByAddressTest(t, 0, "bad format", 400) + pendingTransactionsByAddressTest(t, -1, "json", 400) +} + +func postTransactionTest(t *testing.T, txnToUse, expectedCode int) { + numAccounts := 5 + numTransactions := 5 + offlineAccounts := true + mockLedger, _, _, stxns, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + dummyShutdownChan := make(chan struct{}) + mockNode := makeMockNode(mockLedger, t.Name()) + handler := v2.Handlers{ + Node: mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + e := echo.New() + var body io.Reader + if txnToUse >= 0 { + stxn := stxns[txnToUse] + bodyBytes := protocol.Encode(&stxn) + body = bytes.NewReader(bodyBytes) + } + req := httptest.NewRequest(http.MethodPost, "/", body) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + err := handler.RawTransaction(c) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + +func TestPostTransaction(t *testing.T) { + postTransactionTest(t, -1, 400) + postTransactionTest(t, 0, 200) +} + +func TestStartCatchup(t *testing.T) { + t.Skip("feature not yet deployed") +} + +func TestAbortCatchup(t *testing.T) { + t.Skip("feature not yet deployed") +} + +func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int) { + numAccounts := 1 + numTransactions := 1 + offlineAccounts := true + mockLedger, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + dummyShutdownChan := make(chan struct{}) + mockNode := makeMockNode(mockLedger, t.Name()) + handler := v2.Handlers{ + Node: &mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(bytesToUse)) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + err := handler.TealCompile(c) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} - require.NoError(t, nil) +func TestTealCompile(t *testing.T) { + tealCompileTest(t, nil, 200) // nil program should work + goodProgram := `int 1` + goodProgramBytes := []byte(goodProgram) + tealCompileTest(t, goodProgramBytes, 200) + badProgram := "this is incorrect TEAL" + badProgramBytes := []byte(badProgram) + tealCompileTest(t, badProgramBytes, 400) } diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index a4c2b8082a..2ebf997beb 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -1,19 +1,26 @@ package test import ( + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/node" + "github.com/algorand/go-algorand/node/indexer" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" - "math/rand" - "strconv" - "testing" ) type mockNode struct { @@ -31,7 +38,8 @@ func (m mockNode) Ledger() *data.Ledger { func (m mockNode) Status() (s node.StatusReport, err error) { s = node.StatusReport{ - LastRound: basics.Round(1), + LastRound: basics.Round(1), + LastVersion: protocol.ConsensusCurrentVersion, } return } @@ -54,8 +62,7 @@ func (m mockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWit } func (m mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { - stxns := make([]transactions.SignedTxn, 1) - return stxns, nil + return nil, nil } func (m mockNode) SuggestedFee() basics.MicroAlgos { @@ -63,19 +70,58 @@ func (m mockNode) SuggestedFee() basics.MicroAlgos { } // unused by handlers: -//Config() config.Local -//Start() -//ListeningAddress() (string, bool) -//Stop() -//ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]TxnWithStatus, error) -//GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (TxnWithStatus, bool) -//PoolStats() PoolStats -//IsArchival() bool -//OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) -//Uint64() uint64 -//Indexer() (*indexer.Indexer, error) -//GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) -//AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) +func (m mockNode) Config() config.Local { + return config.GetDefaultLocal() +} +func (m mockNode) Start() {} + +func (m mockNode) ListeningAddress() (string, bool) { + return "mock listening addresses not implemented", false +} + +func (m mockNode) Stop() {} + +func (m mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { + return nil, fmt.Errorf("listtxns not implemented") +} + +func (m mockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { + return node.TxnWithStatus{}, false +} + +func (m mockNode) PoolStats() node.PoolStats { + return node.PoolStats{} +} + +func (m mockNode) IsArchival() bool { + return false +} + +func (m mockNode) OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) {} + +func (m mockNode) Uint64() uint64 { + return 1 +} + +func (m mockNode) Indexer() (*indexer.Indexer, error) { + return nil, fmt.Errorf("indexer not implemented") +} + +func (m mockNode) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (node.TxnWithStatus, error) { + return node.TxnWithStatus{}, fmt.Errorf("get transaction by id not implemented") +} + +func (m mockNode) AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) { + return nil, fmt.Errorf("assemble block not implemented") +} + +func (m mockNode) StartCatchup(catchpoint string) error { + return fmt.Errorf("start catchup not implemented") +} + +func (m mockNode) AbortCatchup(catchpoint string) error { + return fmt.Errorf("abort catchup not implemented") +} ////// mock ledger testing environment follows @@ -153,8 +199,7 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d // generate test transactions const inMem = true - const archival = true - ledger, err := data.LoadLedger(logging.Base(), t.Name(), inMem, protocol.ConsensusCurrentVersion, bootstrap, genesisID, genesisHash, nil, archival) + ledger, err := data.LoadLedger(logging.Base(), t.Name(), inMem, protocol.ConsensusCurrentVersion, bootstrap, genesisID, genesisHash, nil, config.GetDefaultLocal()) if err != nil { panic(err) } @@ -188,7 +233,6 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d amt := basics.MicroAlgos{Raw: uint64(gen.Int() % xferMax)} fee := basics.MicroAlgos{Raw: uint64(gen.Int()%maxFee) + proto.MinTxnFee} - t := transactions.Transaction{ Type: protocol.PaymentTx, Header: transactions.Header{ @@ -206,11 +250,7 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d } rand.Read(t.Note) - //if reallySignTxs { tx[i] = t.Sign(roots[send].Secrets()) - //} else { - // tx[i] = transactions.SignedTxn{Transaction: t, Sig: crypto.Signature{}} - //} sbal := bal[saddr] sbal.MicroAlgos.Raw -= fee.Raw @@ -225,6 +265,5 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d rbal.MicroAlgos.Raw += amt.Raw bal[raddr] = rbal } - return ledger, roots, parts, tx, release } diff --git a/node/node.go b/node/node.go index db26393bcf..dc3857ccbf 100644 --- a/node/node.go +++ b/node/node.go @@ -107,6 +107,8 @@ type NodeInterface interface { Indexer() (*indexer.Indexer, error) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) + StartCatchup(catchpoint string) error + AbortCatchup(catchpoint string) error } // AlgorandFullNode specifies and implements a full Algorand node. From 273757938206c8f50466e561fe3c777c3242433f Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 12:44:31 -0400 Subject: [PATCH 007/267] Obey `make sanity`, except for files not touched by this PR --- daemon/algod/api/server/v2/handlers.go | 2 +- .../algod/api/server/v2/test/handlers_test.go | 17 ++++++++++++++++- daemon/algod/api/server/v2/test/helpers.go | 18 +++++++++++++++++- node/node.go | 4 ++-- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 935bb8efd1..6ed520b875 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -46,7 +46,7 @@ const maxTealSourceBytes = 1e5 // Handlers is an implementation to the V2 route handler interface defined by the generated code. type Handlers struct { - Node node.NodeInterface + Node node.Interface Log logging.Logger Shutdown <-chan struct{} } diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 8f1a533c29..d345d2b530 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -1,8 +1,23 @@ +// 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 . + package test import ( "bytes" - "fmt" "io" "net/http" "net/http/httptest" diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 2ebf997beb..8b387d4776 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -1,3 +1,19 @@ +// 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 . + package test import ( @@ -66,7 +82,7 @@ func (m mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { } func (m mockNode) SuggestedFee() basics.MicroAlgos { - return basics.MicroAlgos{1} + return basics.MicroAlgos{Raw: 1} } // unused by handlers: diff --git a/node/node.go b/node/node.go index dc3857ccbf..daa499726e 100644 --- a/node/node.go +++ b/node/node.go @@ -83,8 +83,8 @@ func (status StatusReport) TimeSinceLastRound() time.Duration { return time.Since(status.LastRoundTimestamp) } -// NodeInterface represents node fns. -type NodeInterface interface { +// Interface represents node fns. +type Interface interface { Ledger() *data.Ledger // used by handlers; ledger is in turn used extensively, so that might need mocking too? Status() (s StatusReport, err error) // used by handlers GenesisID() string // used by handlers From 722232598e6051709db1b9739e184a72d12b54b7 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 12:47:22 -0400 Subject: [PATCH 008/267] unskip statusafterblock --- daemon/algod/api/server/v2/test/handlers_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index d345d2b530..89a0ab087b 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -102,7 +102,6 @@ func TestGetStatus(t *testing.T) { } func TestGetStatusAfterBlock(t *testing.T) { - t.Skip("skipping for now as this waits up to a minute") handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.WaitForBlock(c, 0) From 7437758fbd7f58b0d0d013575573af88744920bb Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 12:55:16 -0400 Subject: [PATCH 009/267] remove notes from Node.Interface --- node/node.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/node/node.go b/node/node.go index daa499726e..b2003e3860 100644 --- a/node/node.go +++ b/node/node.go @@ -85,15 +85,14 @@ func (status StatusReport) TimeSinceLastRound() time.Duration { // Interface represents node fns. type Interface interface { - Ledger() *data.Ledger // used by handlers; ledger is in turn used extensively, so that might need mocking too? - Status() (s StatusReport, err error) // used by handlers - GenesisID() string // used by handlers - GenesisHash() crypto.Digest // used by handlers - BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error // used by handlers - GetPendingTransaction(txID transactions.Txid) (res TxnWithStatus, found bool) // used by handlers - GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) // used by handlers - SuggestedFee() basics.MicroAlgos // used by handlers - // unused by handlers: + Ledger() *data.Ledger + Status() (s StatusReport, err error) + GenesisID() string + GenesisHash() crypto.Digest + BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error + GetPendingTransaction(txID transactions.Txid) (res TxnWithStatus, found bool) + GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) + SuggestedFee() basics.MicroAlgos Config() config.Local Start() ListeningAddress() (string, bool) From 8f185b99a0a0e1b0ccaeb77e4b9b36191ff01dd4 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 13:46:42 -0400 Subject: [PATCH 010/267] remove some functions from node interface relating to other interfaces; move start and abort catchup up --- node/node.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/node/node.go b/node/node.go index b2003e3860..8d06d5c7b8 100644 --- a/node/node.go +++ b/node/node.go @@ -93,6 +93,8 @@ type Interface interface { GetPendingTransaction(txID transactions.Txid) (res TxnWithStatus, found bool) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) SuggestedFee() basics.MicroAlgos + StartCatchup(catchpoint string) error + AbortCatchup(catchpoint string) error Config() config.Local Start() ListeningAddress() (string, bool) @@ -101,13 +103,8 @@ type Interface interface { GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (TxnWithStatus, bool) PoolStats() PoolStats IsArchival() bool - OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) - Uint64() uint64 Indexer() (*indexer.Indexer, error) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) - AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) - StartCatchup(catchpoint string) error - AbortCatchup(catchpoint string) error } // AlgorandFullNode specifies and implements a full Algorand node. From 2bf41bdec63ef488f6845142b6cbf50f45a8a0fa Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 13:53:55 -0400 Subject: [PATCH 011/267] move mock node into components/mocks --- components/mocks/mockNode.go | 103 +++++++++++++++++++ daemon/algod/api/server/v2/test/helpers.go | 109 +-------------------- 2 files changed, 107 insertions(+), 105 deletions(-) create mode 100644 components/mocks/mockNode.go diff --git a/components/mocks/mockNode.go b/components/mocks/mockNode.go new file mode 100644 index 0000000000..cbce4566d6 --- /dev/null +++ b/components/mocks/mockNode.go @@ -0,0 +1,103 @@ +package mocks + +import ( + "fmt" + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/node" + "github.com/algorand/go-algorand/node/indexer" + "github.com/algorand/go-algorand/protocol" +) + +type MockNode struct { + ledger *data.Ledger + genesisID string +} + +func MakeMockNode(ledger *data.Ledger, genesisID string) MockNode { + return MockNode{ledger: ledger, genesisID: genesisID} +} + +func (m MockNode) Ledger() *data.Ledger { + return m.ledger +} + +func (m MockNode) Status() (s node.StatusReport, err error) { + s = node.StatusReport{ + LastRound: basics.Round(1), + LastVersion: protocol.ConsensusCurrentVersion, + } + return +} +func (m MockNode) GenesisID() string { + return m.genesisID +} + +func (m MockNode) GenesisHash() crypto.Digest { + return m.ledger.GenesisHash() +} + +func (m MockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { + return nil +} + +func (m MockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { + res = node.TxnWithStatus{} + found = true + return +} + +func (m MockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { + return nil, nil +} + +func (m MockNode) SuggestedFee() basics.MicroAlgos { + return basics.MicroAlgos{Raw: 1} +} + +func (m MockNode) StartCatchup(catchpoint string) error { + return fmt.Errorf("start catchup not implemented") +} + +func (m MockNode) AbortCatchup(catchpoint string) error { + return fmt.Errorf("abort catchup not implemented") +} + +// unused by handlers: +func (m MockNode) Config() config.Local { + return config.GetDefaultLocal() +} +func (m MockNode) Start() {} + +func (m MockNode) ListeningAddress() (string, bool) { + return "mock listening addresses not implemented", false +} + +func (m MockNode) Stop() {} + +func (m MockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { + return nil, fmt.Errorf("listtxns not implemented") +} + +func (m MockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { + return node.TxnWithStatus{}, false +} + +func (m MockNode) PoolStats() node.PoolStats { + return node.PoolStats{} +} + +func (m MockNode) IsArchival() bool { + return false +} + +func (m MockNode) Indexer() (*indexer.Indexer, error) { + return nil, fmt.Errorf("indexer not implemented") +} + +func (m MockNode) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (node.TxnWithStatus, error) { + return node.TxnWithStatus{}, fmt.Errorf("get transaction by id not implemented") +} diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 8b387d4776..1db7d3e479 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -17,126 +17,25 @@ package test import ( - "fmt" "math/rand" "strconv" "testing" - "time" - "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/components/mocks" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/logging" - "github.com/algorand/go-algorand/node" - "github.com/algorand/go-algorand/node/indexer" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" ) -type mockNode struct { - ledger *data.Ledger - genesisID string -} - -func makeMockNode(ledger *data.Ledger, genesisID string) mockNode { - return mockNode{ledger: ledger, genesisID: genesisID} -} - -func (m mockNode) Ledger() *data.Ledger { - return m.ledger -} - -func (m mockNode) Status() (s node.StatusReport, err error) { - s = node.StatusReport{ - LastRound: basics.Round(1), - LastVersion: protocol.ConsensusCurrentVersion, - } - return -} -func (m mockNode) GenesisID() string { - return m.genesisID -} - -func (m mockNode) GenesisHash() crypto.Digest { - return m.ledger.GenesisHash() -} - -func (m mockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { - return nil -} - -func (m mockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { - res = node.TxnWithStatus{} - found = true - return -} - -func (m mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { - return nil, nil -} - -func (m mockNode) SuggestedFee() basics.MicroAlgos { - return basics.MicroAlgos{Raw: 1} -} - -// unused by handlers: -func (m mockNode) Config() config.Local { - return config.GetDefaultLocal() -} -func (m mockNode) Start() {} - -func (m mockNode) ListeningAddress() (string, bool) { - return "mock listening addresses not implemented", false -} - -func (m mockNode) Stop() {} - -func (m mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { - return nil, fmt.Errorf("listtxns not implemented") -} - -func (m mockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { - return node.TxnWithStatus{}, false -} - -func (m mockNode) PoolStats() node.PoolStats { - return node.PoolStats{} -} - -func (m mockNode) IsArchival() bool { - return false -} - -func (m mockNode) OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) {} - -func (m mockNode) Uint64() uint64 { - return 1 -} - -func (m mockNode) Indexer() (*indexer.Indexer, error) { - return nil, fmt.Errorf("indexer not implemented") -} - -func (m mockNode) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (node.TxnWithStatus, error) { - return node.TxnWithStatus{}, fmt.Errorf("get transaction by id not implemented") -} - -func (m mockNode) AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) { - return nil, fmt.Errorf("assemble block not implemented") -} - -func (m mockNode) StartCatchup(catchpoint string) error { - return fmt.Errorf("start catchup not implemented") -} - -func (m mockNode) AbortCatchup(catchpoint string) error { - return fmt.Errorf("abort catchup not implemented") +func makeMockNode(ledger *data.Ledger, genesisID string) mocks.MockNode { + mockNode := mocks.MakeMockNode(ledger, genesisID) + return mockNode } ////// mock ledger testing environment follows From 33f94d4d3b18d4233b44556b319fa29d73f071bd Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 14:29:25 -0400 Subject: [PATCH 012/267] add tests for start catchup and abort catchup; to do so, the mock start and abort catchup no longer return an error --- components/mocks/mockNode.go | 4 +- .../algod/api/server/v2/test/handlers_test.go | 55 ++++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/components/mocks/mockNode.go b/components/mocks/mockNode.go index cbce4566d6..142dd9870b 100644 --- a/components/mocks/mockNode.go +++ b/components/mocks/mockNode.go @@ -59,11 +59,11 @@ func (m MockNode) SuggestedFee() basics.MicroAlgos { } func (m MockNode) StartCatchup(catchpoint string) error { - return fmt.Errorf("start catchup not implemented") + return nil } func (m MockNode) AbortCatchup(catchpoint string) error { - return fmt.Errorf("abort catchup not implemented") + return nil } // unused by handlers: diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 89a0ab087b..d61bebebdd 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -102,6 +102,7 @@ func TestGetStatus(t *testing.T) { } func TestGetStatusAfterBlock(t *testing.T) { + t.Skip("temporary skipping for local testing, should not make it into PR") handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.WaitForBlock(c, 0) @@ -205,12 +206,62 @@ func TestPostTransaction(t *testing.T) { postTransactionTest(t, 0, 200) } +func startCatchupTest(t *testing.T, catchpoint string, expectedCode int) { + numAccounts := 1 + numTransactions := 1 + offlineAccounts := true + mockLedger, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + dummyShutdownChan := make(chan struct{}) + mockNode := makeMockNode(mockLedger, t.Name()) + handler := v2.Handlers{ + Node: mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + err := handler.StartCatchup(c, catchpoint) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) +} + func TestStartCatchup(t *testing.T) { - t.Skip("feature not yet deployed") + goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" + startCatchupTest(t, goodCatchPoint, 200) + badCatchPoint := "this is an invalid catchpoint" + startCatchupTest(t, badCatchPoint, 400) +} + +func abortCatchupTest(t *testing.T, catchpoint string, expectedCode int) { + numAccounts := 1 + numTransactions := 1 + offlineAccounts := true + mockLedger, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + dummyShutdownChan := make(chan struct{}) + mockNode := makeMockNode(mockLedger, t.Name()) + handler := v2.Handlers{ + Node: mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + e := echo.New() + req := httptest.NewRequest(http.MethodDelete, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + err := handler.StartCatchup(c, catchpoint) + require.NoError(t, err) + require.Equal(t, expectedCode, rec.Code) } func TestAbortCatchup(t *testing.T) { - t.Skip("feature not yet deployed") + goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" + abortCatchupTest(t, goodCatchPoint, 200) + badCatchPoint := "this is an invalid catchpoint" + abortCatchupTest(t, badCatchPoint, 400) } func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int) { From ee8b260596c52907f19f29f0620f3c4711ff6eb6 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 2 Jun 2020 17:13:16 -0400 Subject: [PATCH 013/267] move node interface to handlers.go; and only include interface functions needed by handlers --- daemon/algod/api/server/v2/handlers.go | 17 ++++++++++++++++- node/node.go | 24 ------------------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 6ed520b875..fe2d57b5d4 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -31,6 +31,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/private" + "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" @@ -46,11 +47,25 @@ const maxTealSourceBytes = 1e5 // Handlers is an implementation to the V2 route handler interface defined by the generated code. type Handlers struct { - Node node.Interface + Node NodeInterface Log logging.Logger Shutdown <-chan struct{} } +// Interface represents node fns. +type NodeInterface interface { + Ledger() *data.Ledger + Status() (s node.StatusReport, err error) + GenesisID() string + GenesisHash() crypto.Digest + BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error + GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) + GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) + SuggestedFee() basics.MicroAlgos + StartCatchup(catchpoint string) error + AbortCatchup(catchpoint string) error +} + // RegisterParticipationKeys registers participation keys. // (POST /v2/register-participation-keys/{address}) func (v2 *Handlers) RegisterParticipationKeys(ctx echo.Context, address string, params private.RegisterParticipationKeysParams) error { diff --git a/node/node.go b/node/node.go index 8d06d5c7b8..f765e9aa09 100644 --- a/node/node.go +++ b/node/node.go @@ -83,30 +83,6 @@ func (status StatusReport) TimeSinceLastRound() time.Duration { return time.Since(status.LastRoundTimestamp) } -// Interface represents node fns. -type Interface interface { - Ledger() *data.Ledger - Status() (s StatusReport, err error) - GenesisID() string - GenesisHash() crypto.Digest - BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error - GetPendingTransaction(txID transactions.Txid) (res TxnWithStatus, found bool) - GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) - SuggestedFee() basics.MicroAlgos - StartCatchup(catchpoint string) error - AbortCatchup(catchpoint string) error - Config() config.Local - Start() - ListeningAddress() (string, bool) - Stop() - ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]TxnWithStatus, error) - GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (TxnWithStatus, bool) - PoolStats() PoolStats - IsArchival() bool - Indexer() (*indexer.Indexer, error) - GetTransactionByID(txid transactions.Txid, rnd basics.Round) (TxnWithStatus, error) -} - // AlgorandFullNode specifies and implements a full Algorand node. type AlgorandFullNode struct { nodeContextData From 703522fba3ad57b877241403389b3cd09ac787a8 Mon Sep 17 00:00:00 2001 From: btoll Date: Wed, 3 Jun 2020 11:47:10 -0400 Subject: [PATCH 014/267] Remove unused var in compute_branch_channel.sh (#1118) Summary While working on the indexer pipeline, shellcheck found an unused variable in scripts/compute_branch_channel.sh, which this commit removes. Test Plan Nothing to test. --- scripts/compute_branch_channel.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/compute_branch_channel.sh b/scripts/compute_branch_channel.sh index cf80fbb396..90cde3b553 100755 --- a/scripts/compute_branch_channel.sh +++ b/scripts/compute_branch_channel.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash -# If enlistment isn't clean, it's 'dev' -CWD=$(cd "$(dirname "$0")" && pwd -P) +# If workspace isn't clean, it's 'dev'. if [ "$1" = "master" ]; then echo "master" From d3ed3d40007daf754c24bb9154a7b738bbc3a63c Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 3 Jun 2020 12:27:40 -0400 Subject: [PATCH 015/267] Update the msgp auto-generated code (#1125) Summary On the header of the generated files, create a summary of which objects and function are implemented. On each of the generated test files, a build tag was added. This buildtag could be used to exclude the build of the unit tests ( needed for testing of encoding using non-test code ) Test Plan No code change presents in this PR. --- agreement/msgp_gen.go | 162 +++++++++++++++++++++++ agreement/msgp_gen_test.go | 2 + auction/msgp_gen.go | 106 +++++++++++++++ auction/msgp_gen_test.go | 2 + crypto/msgp_gen.go | 202 +++++++++++++++++++++++++++++ crypto/msgp_gen_test.go | 2 + data/basics/msgp_gen.go | 74 +++++++++++ data/basics/msgp_gen_test.go | 2 + data/bookkeeping/msgp_gen.go | 58 +++++++++ data/bookkeeping/msgp_gen_test.go | 2 + data/committee/msgp_gen.go | 34 +++++ data/committee/msgp_gen_test.go | 2 + data/hashable/msgp_gen.go | 10 ++ data/hashable/msgp_gen_test.go | 2 + data/transactions/msgp_gen.go | 130 +++++++++++++++++++ data/transactions/msgp_gen_test.go | 2 + go.mod | 2 +- go.sum | 4 +- ledger/msgp_gen.go | 58 +++++++++ ledger/msgp_gen_test.go | 2 + node/msgp_gen.go | 18 +++ node/msgp_gen_test.go | 2 + protocol/msgp_gen.go | 50 +++++++ protocol/msgp_gen_test.go | 2 + rpcs/msgp_gen.go | 10 ++ rpcs/msgp_gen_test.go | 2 + 26 files changed, 939 insertions(+), 3 deletions(-) diff --git a/agreement/msgp_gen.go b/agreement/msgp_gen.go index cb534f001a..1d0b921f7a 100644 --- a/agreement/msgp_gen.go +++ b/agreement/msgp_gen.go @@ -7,6 +7,168 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Certificate +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// bundle +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// equivocationVote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// equivocationVoteAuthenticator +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// period +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// proposal +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// proposalValue +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// proposerSeed +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// rawVote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// seedInput +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// selector +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// serializableErrorUnderlying +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// step +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// transmittedPayload +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// unauthenticatedBundle +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// unauthenticatedEquivocationVote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// unauthenticatedProposal +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// unauthenticatedVote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// vote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// voteAuthenticator +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Certificate) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/agreement/msgp_gen_test.go b/agreement/msgp_gen_test.go index 84371a6391..0fcb8ae376 100644 --- a/agreement/msgp_gen_test.go +++ b/agreement/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package agreement // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/auction/msgp_gen.go b/auction/msgp_gen.go index 2c8afc131a..3bd1f3bb6c 100644 --- a/auction/msgp_gen.go +++ b/auction/msgp_gen.go @@ -6,6 +6,112 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Bid +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BidOutcomes +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BidderOutcome +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Deposit +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// MasterInput +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// NoteField +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// NoteFieldType +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// Params +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Settlement +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedBid +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedDeposit +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedParams +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedSettlement +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Bid) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/auction/msgp_gen_test.go b/auction/msgp_gen_test.go index 97056ad80a..72d88cc482 100644 --- a/auction/msgp_gen_test.go +++ b/auction/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package auction // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/crypto/msgp_gen.go b/crypto/msgp_gen.go index c863fa25e0..c390e7ef70 100644 --- a/crypto/msgp_gen.go +++ b/crypto/msgp_gen.go @@ -9,6 +9,208 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Digest +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// MasterDerivationKey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// MultisigSig +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// MultisigSubsig +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignature +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignatureSecrets +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignatureSecretsPersistent +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignatureSubkeyBatchID +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignatureSubkeyOffsetID +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// OneTimeSignatureVerifier +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// PrivateKey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// PublicKey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Seed +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Signature +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignatureSecrets +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// VRFSecrets +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// VrfOutput +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// VrfPrivkey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// VrfProof +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// VrfPubkey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ed25519PrivateKey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ed25519PublicKey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ed25519Seed +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ed25519Signature +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ephemeralSubkey +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Digest) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/crypto/msgp_gen_test.go b/crypto/msgp_gen_test.go index cdf29cb7d4..8dcd1ed9d7 100644 --- a/crypto/msgp_gen_test.go +++ b/crypto/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package crypto // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/data/basics/msgp_gen.go b/data/basics/msgp_gen.go index e34ca3b941..479bf8e329 100644 --- a/data/basics/msgp_gen.go +++ b/data/basics/msgp_gen.go @@ -9,6 +9,80 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// AccountData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Address +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AssetHolding +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AssetIndex +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// AssetParams +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BalanceRecord +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Round +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// RoundInterval +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// Status +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/basics/msgp_gen_test.go b/data/basics/msgp_gen_test.go index c6cce16926..5e706cfb78 100644 --- a/data/basics/msgp_gen_test.go +++ b/data/basics/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package basics // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/data/bookkeeping/msgp_gen.go b/data/bookkeeping/msgp_gen.go index 8d6b873653..06d61d8ca1 100644 --- a/data/bookkeeping/msgp_gen.go +++ b/data/bookkeeping/msgp_gen.go @@ -7,6 +7,64 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Block +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BlockHash +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// BlockHeader +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Genesis +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// GenesisAllocation +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// RewardsState +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// UpgradeVote +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Block) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/bookkeeping/msgp_gen_test.go b/data/bookkeeping/msgp_gen_test.go index 05a75ab839..3193b395e8 100644 --- a/data/bookkeeping/msgp_gen_test.go +++ b/data/bookkeeping/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package bookkeeping // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/data/committee/msgp_gen.go b/data/committee/msgp_gen.go index 845cbcfe34..36799db60f 100644 --- a/data/committee/msgp_gen.go +++ b/data/committee/msgp_gen.go @@ -6,6 +6,40 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Credential +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Seed +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// UnauthenticatedCredential +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// hashableCredential +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Credential) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/committee/msgp_gen_test.go b/data/committee/msgp_gen_test.go index cdbad86c93..c0f1780fc9 100644 --- a/data/committee/msgp_gen_test.go +++ b/data/committee/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package committee // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/data/hashable/msgp_gen.go b/data/hashable/msgp_gen.go index 4322867649..edcd71023d 100644 --- a/data/hashable/msgp_gen.go +++ b/data/hashable/msgp_gen.go @@ -6,6 +6,16 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// Message +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *Message) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/hashable/msgp_gen_test.go b/data/hashable/msgp_gen_test.go index 3a3617154a..ada098eb9b 100644 --- a/data/hashable/msgp_gen_test.go +++ b/data/hashable/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package hashable // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index 6a93bea567..1ea652529c 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -7,6 +7,136 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// ApplyData +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AssetConfigTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AssetFreezeTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AssetTransferTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Header +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// KeyregTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// LogicSig +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// MinFeeError +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// PaymentTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Payset +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// SignedTxn +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedTxnInBlock +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// SignedTxnWithAD +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Transaction +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// TxGroup +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// Txid +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *ApplyData) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/transactions/msgp_gen_test.go b/data/transactions/msgp_gen_test.go index 6def5d1e62..06bed4c0af 100644 --- a/data/transactions/msgp_gen_test.go +++ b/data/transactions/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package transactions // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/go.mod b/go.mod index d75b0cb74c..ae701c45fc 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 - github.com/algorand/msgp v1.1.40 + github.com/algorand/msgp v1.1.43 github.com/algorand/oapi-codegen v1.3.5-algorand4 github.com/algorand/websocket v1.4.1 github.com/aws/aws-sdk-go v1.16.5 diff --git a/go.sum b/go.sum index 074f183320..a3b151b2fe 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d h1:W9MgGUo github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d/go.mod h1:qm6LyXvDa1+uZJxaVg8X+OEjBqt/zDinDa2EohtTDxU= github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 h1:IiCuOE1YCReVyEr1IQHKTBTvFLKdeBCfQuxrqhniq+I= github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759/go.mod h1:Kve3O9VpxZIHsPzpfxNdyFltFU9jBTeVYMYxSC99tdg= -github.com/algorand/msgp v1.1.40 h1:GljBzSmwUhlRSA/y/U3hlMcR2WCisW6E1s6B4lwExow= -github.com/algorand/msgp v1.1.40/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= +github.com/algorand/msgp v1.1.43 h1:VPGGwJ5zIg9lRqgL23irmfcxgvgho1dpx7Lq6X2sn8g= +github.com/algorand/msgp v1.1.43/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= github.com/algorand/oapi-codegen v1.3.5-algorand4 h1:skjv/lhlWAGnQPHQ9qdzTE5Bk9W555EKrh1bSul0JJs= github.com/algorand/oapi-codegen v1.3.5-algorand4/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= github.com/algorand/websocket v1.4.1 h1:FPoNHI8i2VZWZzhCscY8JTzsAE7Vv73753cMbzb3udk= diff --git a/ledger/msgp_gen.go b/ledger/msgp_gen.go index 9a45cc29ff..e0def7af95 100644 --- a/ledger/msgp_gen.go +++ b/ledger/msgp_gen.go @@ -6,6 +6,64 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// AccountTotals +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AlgoCount +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// CatchpointCatchupState +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// catchpointFileBalancesChunk +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// catchpointFileHeader +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// catchpointState +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// encodedBalanceRecord +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *AccountTotals) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/ledger/msgp_gen_test.go b/ledger/msgp_gen_test.go index a13b233bbc..63e2b8be22 100644 --- a/ledger/msgp_gen_test.go +++ b/ledger/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package ledger // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/node/msgp_gen.go b/node/msgp_gen.go index 7257a3e397..b01f513869 100644 --- a/node/msgp_gen.go +++ b/node/msgp_gen.go @@ -6,6 +6,24 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// netPrioResponse +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// netPrioResponseSigned +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *netPrioResponse) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/node/msgp_gen_test.go b/node/msgp_gen_test.go index 170a47393a..f41ed5ec98 100644 --- a/node/msgp_gen_test.go +++ b/node/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package node // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/protocol/msgp_gen.go b/protocol/msgp_gen.go index 2961c4fc72..99580e96de 100644 --- a/protocol/msgp_gen.go +++ b/protocol/msgp_gen.go @@ -6,6 +6,56 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// ConsensusVersion +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// Error +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// HashID +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// NetworkID +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// Tag +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// TxType +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z ConsensusVersion) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/protocol/msgp_gen_test.go b/protocol/msgp_gen_test.go index 4e0876303a..4bdc805908 100644 --- a/protocol/msgp_gen_test.go +++ b/protocol/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package protocol // Code generated by github.com/algorand/msgp DO NOT EDIT. diff --git a/rpcs/msgp_gen.go b/rpcs/msgp_gen.go index 84ae94b9e3..79dfd7b4d3 100644 --- a/rpcs/msgp_gen.go +++ b/rpcs/msgp_gen.go @@ -6,6 +6,16 @@ import ( "github.com/algorand/msgp/msgp" ) +// The following msgp objects are implemented in this file: +// EncodedBlockCert +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// + // MarshalMsg implements msgp.Marshaler func (z *EncodedBlockCert) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/rpcs/msgp_gen_test.go b/rpcs/msgp_gen_test.go index 7559695a81..7f1c0de8c2 100644 --- a/rpcs/msgp_gen_test.go +++ b/rpcs/msgp_gen_test.go @@ -1,3 +1,5 @@ +// +build !skip_msgp_testing + package rpcs // Code generated by github.com/algorand/msgp DO NOT EDIT. From b4cb747ac481f902b426040fba491aca76291b4d Mon Sep 17 00:00:00 2001 From: John Lee Date: Wed, 3 Jun 2020 12:28:24 -0400 Subject: [PATCH 016/267] Bugfix - build_packages should be build-packages (#1123) There was an error in the forward GPG agent script, that caused the SSH key copying to fail. --- scripts/release/forward_gpg_agent.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/forward_gpg_agent.sh b/scripts/release/forward_gpg_agent.sh index 1a0e7ff94f..458c72510f 100755 --- a/scripts/release/forward_gpg_agent.sh +++ b/scripts/release/forward_gpg_agent.sh @@ -9,7 +9,7 @@ then exit 1 fi -BRANCH=build_packages +BRANCH=build-packages EC2_INSTANCE_KEY=ReleaseBuildInstanceKey.pem # Get the ec2 instance name and the ephemeral private key from the Jenkins server. From bd46fd34d401b03cefbe59a300db53c376da2455 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 13:37:04 -0400 Subject: [PATCH 017/267] Revert "move mock node into components/mocks" This reverts commit 2bf41bdec63ef488f6845142b6cbf50f45a8a0fa. --- components/mocks/mockNode.go | 103 ------------------- daemon/algod/api/server/v2/test/helpers.go | 109 ++++++++++++++++++++- 2 files changed, 105 insertions(+), 107 deletions(-) delete mode 100644 components/mocks/mockNode.go diff --git a/components/mocks/mockNode.go b/components/mocks/mockNode.go deleted file mode 100644 index 142dd9870b..0000000000 --- a/components/mocks/mockNode.go +++ /dev/null @@ -1,103 +0,0 @@ -package mocks - -import ( - "fmt" - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/data" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/node" - "github.com/algorand/go-algorand/node/indexer" - "github.com/algorand/go-algorand/protocol" -) - -type MockNode struct { - ledger *data.Ledger - genesisID string -} - -func MakeMockNode(ledger *data.Ledger, genesisID string) MockNode { - return MockNode{ledger: ledger, genesisID: genesisID} -} - -func (m MockNode) Ledger() *data.Ledger { - return m.ledger -} - -func (m MockNode) Status() (s node.StatusReport, err error) { - s = node.StatusReport{ - LastRound: basics.Round(1), - LastVersion: protocol.ConsensusCurrentVersion, - } - return -} -func (m MockNode) GenesisID() string { - return m.genesisID -} - -func (m MockNode) GenesisHash() crypto.Digest { - return m.ledger.GenesisHash() -} - -func (m MockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { - return nil -} - -func (m MockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { - res = node.TxnWithStatus{} - found = true - return -} - -func (m MockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { - return nil, nil -} - -func (m MockNode) SuggestedFee() basics.MicroAlgos { - return basics.MicroAlgos{Raw: 1} -} - -func (m MockNode) StartCatchup(catchpoint string) error { - return nil -} - -func (m MockNode) AbortCatchup(catchpoint string) error { - return nil -} - -// unused by handlers: -func (m MockNode) Config() config.Local { - return config.GetDefaultLocal() -} -func (m MockNode) Start() {} - -func (m MockNode) ListeningAddress() (string, bool) { - return "mock listening addresses not implemented", false -} - -func (m MockNode) Stop() {} - -func (m MockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { - return nil, fmt.Errorf("listtxns not implemented") -} - -func (m MockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { - return node.TxnWithStatus{}, false -} - -func (m MockNode) PoolStats() node.PoolStats { - return node.PoolStats{} -} - -func (m MockNode) IsArchival() bool { - return false -} - -func (m MockNode) Indexer() (*indexer.Indexer, error) { - return nil, fmt.Errorf("indexer not implemented") -} - -func (m MockNode) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (node.TxnWithStatus, error) { - return node.TxnWithStatus{}, fmt.Errorf("get transaction by id not implemented") -} diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 1db7d3e479..8b387d4776 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -17,25 +17,126 @@ package test import ( + "fmt" "math/rand" "strconv" "testing" + "time" - "github.com/algorand/go-algorand/components/mocks" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/node" + "github.com/algorand/go-algorand/node/indexer" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/db" ) -func makeMockNode(ledger *data.Ledger, genesisID string) mocks.MockNode { - mockNode := mocks.MakeMockNode(ledger, genesisID) - return mockNode +type mockNode struct { + ledger *data.Ledger + genesisID string +} + +func makeMockNode(ledger *data.Ledger, genesisID string) mockNode { + return mockNode{ledger: ledger, genesisID: genesisID} +} + +func (m mockNode) Ledger() *data.Ledger { + return m.ledger +} + +func (m mockNode) Status() (s node.StatusReport, err error) { + s = node.StatusReport{ + LastRound: basics.Round(1), + LastVersion: protocol.ConsensusCurrentVersion, + } + return +} +func (m mockNode) GenesisID() string { + return m.genesisID +} + +func (m mockNode) GenesisHash() crypto.Digest { + return m.ledger.GenesisHash() +} + +func (m mockNode) BroadcastSignedTxGroup(txgroup []transactions.SignedTxn) error { + return nil +} + +func (m mockNode) GetPendingTransaction(txID transactions.Txid) (res node.TxnWithStatus, found bool) { + res = node.TxnWithStatus{} + found = true + return +} + +func (m mockNode) GetPendingTxnsFromPool() ([]transactions.SignedTxn, error) { + return nil, nil +} + +func (m mockNode) SuggestedFee() basics.MicroAlgos { + return basics.MicroAlgos{Raw: 1} +} + +// unused by handlers: +func (m mockNode) Config() config.Local { + return config.GetDefaultLocal() +} +func (m mockNode) Start() {} + +func (m mockNode) ListeningAddress() (string, bool) { + return "mock listening addresses not implemented", false +} + +func (m mockNode) Stop() {} + +func (m mockNode) ListTxns(addr basics.Address, minRound basics.Round, maxRound basics.Round) ([]node.TxnWithStatus, error) { + return nil, fmt.Errorf("listtxns not implemented") +} + +func (m mockNode) GetTransaction(addr basics.Address, txID transactions.Txid, minRound basics.Round, maxRound basics.Round) (node.TxnWithStatus, bool) { + return node.TxnWithStatus{}, false +} + +func (m mockNode) PoolStats() node.PoolStats { + return node.PoolStats{} +} + +func (m mockNode) IsArchival() bool { + return false +} + +func (m mockNode) OnNewBlock(block bookkeeping.Block, delta ledger.StateDelta) {} + +func (m mockNode) Uint64() uint64 { + return 1 +} + +func (m mockNode) Indexer() (*indexer.Indexer, error) { + return nil, fmt.Errorf("indexer not implemented") +} + +func (m mockNode) GetTransactionByID(txid transactions.Txid, rnd basics.Round) (node.TxnWithStatus, error) { + return node.TxnWithStatus{}, fmt.Errorf("get transaction by id not implemented") +} + +func (m mockNode) AssembleBlock(round basics.Round, deadline time.Time) (agreement.ValidatedBlock, error) { + return nil, fmt.Errorf("assemble block not implemented") +} + +func (m mockNode) StartCatchup(catchpoint string) error { + return fmt.Errorf("start catchup not implemented") +} + +func (m mockNode) AbortCatchup(catchpoint string) error { + return fmt.Errorf("abort catchup not implemented") } ////// mock ledger testing environment follows From 086b0985698364aa313783ce706713f3a9d8ffd0 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 3 Jun 2020 13:38:08 -0400 Subject: [PATCH 018/267] Make sure node.Status() doesn't access the ledger in catchpoint catchup mode. (#1130) Summary Make sure node.Status() doesn't access the ledger in catchpoint catchup mode. When the node is reloading the ledger at the end of the catchpoint catchup process, there is a short window of time where the blockQ is updated. Since this variable is not synchronized, we could run into a data race. In this particular case, avoiding the ledger completely is pretty straight forward. --- catchup/catchpointService.go | 25 ++++++++++++++++++++----- node/node.go | 36 +++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/catchup/catchpointService.go b/catchup/catchpointService.go index 91d1603d32..4c8bbd3407 100644 --- a/catchup/catchpointService.go +++ b/catchup/catchpointService.go @@ -64,6 +64,8 @@ type CatchpointCatchupService struct { newService bool // indicates whether this service was created after the node was running ( i.e. true ) or the node just started to find that it was previously perfoming catchup net network.GossipNode ledger *ledger.Ledger + // lastBlockHeader is the latest block we have before going into catchpoint catchup mode. We use it to serve the node status requests instead of going to the ledger. + lastBlockHeader bookkeeping.BlockHeader } const ( @@ -76,8 +78,8 @@ const ( ) // MakeResumedCatchpointCatchupService creates a catchpoint catchup service for a node that is already in catchpoint catchup mode -func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (*CatchpointCatchupService, error) { - service := &CatchpointCatchupService{ +func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (service *CatchpointCatchupService, err error) { + service = &CatchpointCatchupService{ stats: CatchpointCatchupStats{ StartTime: time.Now(), }, @@ -88,7 +90,11 @@ func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCat net: net, ledger: l, } - err := service.loadStateVariables(ctx) + service.lastBlockHeader, err = l.BlockHdr(l.Latest()) + if err != nil { + return nil, err + } + err = service.loadStateVariables(ctx) if err != nil { return nil, err } @@ -97,11 +103,11 @@ func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCat } // MakeNewCatchpointCatchupService creates a new catchpoint catchup service for a node that is not in catchpoint catchup mode -func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (*CatchpointCatchupService, error) { +func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (service *CatchpointCatchupService, err error) { if catchpoint == "" { return nil, fmt.Errorf("MakeNewCatchpointCatchupService: catchpoint is invalid") } - service := &CatchpointCatchupService{ + service = &CatchpointCatchupService{ stats: CatchpointCatchupStats{ CatchpointLabel: catchpoint, StartTime: time.Now(), @@ -114,6 +120,10 @@ func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNo net: net, ledger: l, } + service.lastBlockHeader, err = l.BlockHdr(l.Latest()) + if err != nil { + return nil, err + } return service, nil } @@ -571,3 +581,8 @@ func (cs *CatchpointCatchupService) updateBlockRetrievalStatistics(aquiredBlocks cs.stats.AcquiredBlocks = uint64(int64(cs.stats.AcquiredBlocks) + aquiredBlocksDelta) cs.stats.VerifiedBlocks = uint64(int64(cs.stats.VerifiedBlocks) + verifiedBlocksDelta) } + +// GetLatestBlockHeader returns the last block header that was available at the time the catchpoint catchup service started +func (cs *CatchpointCatchupService) GetLatestBlockHeader() bookkeeping.BlockHeader { + return cs.lastBlockHeader +} diff --git a/node/node.go b/node/node.go index f765e9aa09..47ab9a6df4 100644 --- a/node/node.go +++ b/node/node.go @@ -583,23 +583,25 @@ func (node *AlgorandFullNode) GetPendingTransaction(txID transactions.Txid) (res // Status returns a StatusReport structure reporting our status as Active and with our ledger's LastRound func (node *AlgorandFullNode) Status() (s StatusReport, err error) { - s.LastRound = node.ledger.Latest() - b, err := node.ledger.BlockHdr(s.LastRound) - if err != nil { - return - } - node.mu.Lock() defer node.mu.Unlock() - s.LastVersion = b.CurrentProtocol - s.NextVersion, s.NextVersionRound, s.NextVersionSupported = b.NextVersionInfo() - s.LastRoundTimestamp = node.lastRoundTimestamp s.HasSyncedSinceStartup = node.hasSyncedSinceStartup - s.StoppedAtUnsupportedRound = s.LastRound+1 == s.NextVersionRound && !s.NextVersionSupported - s.LastCatchpoint = node.ledger.GetLastCatchpointLabel() + if node.catchpointCatchupService != nil { + // we're in catchpoint catchup mode. + lastBlockHeader := node.catchpointCatchupService.GetLatestBlockHeader() + s.LastRound = lastBlockHeader.Round + s.LastVersion = lastBlockHeader.CurrentProtocol + s.NextVersion, s.NextVersionRound, s.NextVersionSupported = lastBlockHeader.NextVersionInfo() + s.StoppedAtUnsupportedRound = s.LastRound+1 == s.NextVersionRound && !s.NextVersionSupported + + // for now, I'm leaving this commented out. Once we refactor some of the ledger locking mechanisms, we + // should be able to make this call work. + //s.LastCatchpoint = node.ledger.GetLastCatchpointLabel() + + // report back the catchpoint catchup progress statistics stats := node.catchpointCatchupService.GetStatistics() s.Catchpoint = stats.CatchpointLabel s.CatchpointCatchupTotalAccounts = stats.TotalAccounts @@ -608,6 +610,18 @@ func (node *AlgorandFullNode) Status() (s StatusReport, err error) { s.CatchpointCatchupAcquiredBlocks = stats.AcquiredBlocks s.CatchupTime = time.Now().Sub(stats.StartTime) } else { + // we're not in catchpoint catchup mode + var b bookkeeping.BlockHeader + s.LastRound = node.ledger.Latest() + b, err = node.ledger.BlockHdr(s.LastRound) + if err != nil { + return + } + s.LastVersion = b.CurrentProtocol + s.NextVersion, s.NextVersionRound, s.NextVersionSupported = b.NextVersionInfo() + + s.StoppedAtUnsupportedRound = s.LastRound+1 == s.NextVersionRound && !s.NextVersionSupported + s.LastCatchpoint = node.ledger.GetLastCatchpointLabel() s.SynchronizingTime = node.catchupService.SynchronizingTime() s.CatchupTime = node.catchupService.SynchronizingTime() } From ffaa641c5384de40e8a21f4a121fcc4d0387d6e8 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 13:38:16 -0400 Subject: [PATCH 019/267] Add comment explaining mockNode's unusual location --- daemon/algod/api/server/v2/test/helpers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 8b387d4776..79cd4bd906 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -39,6 +39,9 @@ import ( "github.com/algorand/go-algorand/util/db" ) +// ordinarily mockNode would live in `components/mocks` +// but doing this would create an import cycle, as mockNode needs +// package `data` and package `node`, which themselves import `mocks` type mockNode struct { ledger *data.Ledger genesisID string From 67cf394cdffc9496c2ab62982741d4a9737a8f85 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 15:16:24 -0400 Subject: [PATCH 020/267] re-sanity --- daemon/algod/api/server/v2/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index fe2d57b5d4..4567a03c28 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -52,7 +52,7 @@ type Handlers struct { Shutdown <-chan struct{} } -// Interface represents node fns. +// NodeInterface represents node fns used by the handlers. type NodeInterface interface { Ledger() *data.Ledger Status() (s node.StatusReport, err error) From b7bf6f736e6fe3c121f1f984995660b127c44282 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 15:54:23 -0400 Subject: [PATCH 021/267] un-revert start and abort catchup in mocknode, which should return nil --- daemon/algod/api/server/v2/test/handlers_test.go | 10 ++++++++++ daemon/algod/api/server/v2/test/helpers.go | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index d61bebebdd..c076e93a7b 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -65,6 +65,16 @@ func accountInformationTest(t *testing.T, address string, expectedCode int) { err := handler.AccountInformation(c, address) require.NoError(t, err) require.Equal(t, expectedCode, rec.Code) + if expectedCode != 200 { + // response will be known-bad, so don't bother validating response + return + } + expectedResponse := generatedV2.AccountResponse{} + actualResponse := generatedV2.AccountResponse{} + decoder := protocol.NewDecoderBytes(rec.Body.Bytes()) + err = decoder.Decode(&actualResponse) + require.NoError(t, err) + require.Equal(t, expectedResponse, actualResponse) } func TestAccountInformation(t *testing.T) { diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 79cd4bd906..9b187abdbe 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -135,11 +135,11 @@ func (m mockNode) AssembleBlock(round basics.Round, deadline time.Time) (agreeme } func (m mockNode) StartCatchup(catchpoint string) error { - return fmt.Errorf("start catchup not implemented") + return nil } func (m mockNode) AbortCatchup(catchpoint string) error { - return fmt.Errorf("abort catchup not implemented") + return nil } ////// mock ledger testing environment follows From 9e936b871dfb44b72a8da2cd3f6680cbee817fc6 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 3 Jun 2020 15:57:18 -0400 Subject: [PATCH 022/267] Allow updater to request any package with 'gettools' subcommand. (#1131) Updater was almost a general purpose algorand package fetcher. Add the missing "--package" option so that we can use it to fetch other packages (I don't think the third package has been deployed yet, but it is why I want this new option) --- cmd/updater/toolsCmd.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/updater/toolsCmd.go b/cmd/updater/toolsCmd.go index 251aed37d2..0a445b9254 100644 --- a/cmd/updater/toolsCmd.go +++ b/cmd/updater/toolsCmd.go @@ -26,10 +26,12 @@ import ( var toolsDestFile string var toolsBucket string +var toolsPackage string func init() { getToolsCmd.Flags().StringVarP(&toolsDestFile, "outputFile", "o", "", "Path for downloaded file (required)") getToolsCmd.Flags().StringVarP(&toolsBucket, "bucket", "b", "", "S3 bucket to check for tools.") + getToolsCmd.Flags().StringVarP(&toolsPackage, "package", "p", "tools", "Download a specific package.") getToolsCmd.MarkFlagRequired("outputFile") } @@ -46,7 +48,7 @@ var getToolsCmd = &cobra.Command{ exitErrorf("Error creating s3 session %s\n", err.Error()) } - version, name, err := s3Session.GetPackageVersion(channel, "tools", specificVersion) + version, name, err := s3Session.GetPackageVersion(channel, toolsPackage, specificVersion) if err != nil { exitErrorf("Error getting latest tools version from s3 %s\n", err.Error()) } From 925b1f98211f60428ff5ef83f99eb18c58dab2a9 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 16:24:29 -0400 Subject: [PATCH 023/267] fill out a status check golden and an account information golden --- .../algod/api/server/v2/test/handlers_test.go | 40 ++++++++++++++----- daemon/algod/api/server/v2/test/helpers.go | 34 ++++++++++++++-- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index c076e93a7b..4141349560 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -65,20 +65,17 @@ func accountInformationTest(t *testing.T, address string, expectedCode int) { err := handler.AccountInformation(c, address) require.NoError(t, err) require.Equal(t, expectedCode, rec.Code) - if expectedCode != 200 { - // response will be known-bad, so don't bother validating response - return + if address == poolAddr.String() { + expectedResponse := poolAddrResponseGolden + actualResponse := generatedV2.AccountResponse{} + err = protocol.DecodeJSON(rec.Body.Bytes(), &actualResponse) + require.NoError(t, err) + require.Equal(t, expectedResponse, actualResponse) } - expectedResponse := generatedV2.AccountResponse{} - actualResponse := generatedV2.AccountResponse{} - decoder := protocol.NewDecoderBytes(rec.Body.Bytes()) - err = decoder.Decode(&actualResponse) - require.NoError(t, err) - require.Equal(t, expectedResponse, actualResponse) } func TestAccountInformation(t *testing.T) { - accountInformationTest(t, "ALGORANDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIN5DNAU", 200) + accountInformationTest(t, poolAddr.String(), 200) accountInformationTest(t, "malformed", 400) } @@ -105,10 +102,31 @@ func TestGetSupply(t *testing.T) { } func TestGetStatus(t *testing.T) { - handler, c, _, _, _, releasefunc := setupTestForMethodGet(t) + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.GetStatus(c) require.NoError(t, err) + stat := cannedStatusReportGolden + expectedResult := generatedV2.NodeStatusResponse{ + LastRound: uint64(stat.LastRound), + LastVersion: string(stat.LastVersion), + NextVersion: string(stat.NextVersion), + NextVersionRound: uint64(stat.NextVersionRound), + NextVersionSupported: stat.NextVersionSupported, + TimeSinceLastRound: uint64(stat.TimeSinceLastRound().Nanoseconds()), + CatchupTime: uint64(stat.CatchupTime.Nanoseconds()), + StoppedAtUnsupportedRound: stat.StoppedAtUnsupportedRound, + LastCatchpoint: &stat.LastCatchpoint, + Catchpoint: &stat.Catchpoint, + CatchpointTotalAccounts: &stat.CatchpointCatchupTotalAccounts, + CatchpointProcessedAccounts: &stat.CatchpointCatchupProcessedAccounts, + CatchpointTotalBlocks: &stat.CatchpointCatchupTotalBlocks, + CatchpointAcquiredBlocks: &stat.CatchpointCatchupAcquiredBlocks, + } + actualResult := generatedV2.NodeStatusResponse{} + err = protocol.DecodeJSON(rec.Body.Bytes(), &actualResult) + require.NoError(t, err) + require.Equal(t, expectedResult, actualResult) } func TestGetStatusAfterBlock(t *testing.T) { diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 9b187abdbe..ef728d42ac 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -26,6 +26,7 @@ import ( "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" + generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/account" "github.com/algorand/go-algorand/data/basics" @@ -39,6 +40,34 @@ import ( "github.com/algorand/go-algorand/util/db" ) +var cannedStatusReportGolden = node.StatusReport{ + LastRound: basics.Round(1), + LastVersion: protocol.ConsensusCurrentVersion, + NextVersion: protocol.ConsensusCurrentVersion, + NextVersionRound: basics.Round(1), + NextVersionSupported: true, + StoppedAtUnsupportedRound: true, + Catchpoint: "", + CatchpointCatchupAcquiredBlocks: 0, + CatchpointCatchupProcessedAccounts: 0, + CatchpointCatchupTotalAccounts: 0, + CatchpointCatchupTotalBlocks: 0, + LastCatchpoint: "", +} + +var poolAddrRewardBaseGolden = uint64(0) +var poolAddrAssetsGolden = make([]generatedV2.AssetHolding, 0) +var poolAddrCreatedAssetsGolden = make([]generatedV2.Asset, 0) +var poolAddrResponseGolden = generatedV2.AccountResponse{ + Address: poolAddr.String(), + Amount: 50000000000, + AmountWithoutPendingRewards: 50000000000, + Assets: &poolAddrAssetsGolden, + CreatedAssets: &poolAddrCreatedAssetsGolden, + RewardBase: &poolAddrRewardBaseGolden, + Status: "Not Participating", +} + // ordinarily mockNode would live in `components/mocks` // but doing this would create an import cycle, as mockNode needs // package `data` and package `node`, which themselves import `mocks` @@ -56,10 +85,7 @@ func (m mockNode) Ledger() *data.Ledger { } func (m mockNode) Status() (s node.StatusReport, err error) { - s = node.StatusReport{ - LastRound: basics.Round(1), - LastVersion: protocol.ConsensusCurrentVersion, - } + s = cannedStatusReportGolden return } func (m mockNode) GenesisID() string { From cff72104ac57c6ffd0a3423662afbaceed5bc78d Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 3 Jun 2020 16:24:53 -0400 Subject: [PATCH 024/267] fill out a status check golden and an account information golden remove skip in statusafterblock --- daemon/algod/api/server/v2/test/handlers_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 4141349560..08180208fd 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -130,7 +130,6 @@ func TestGetStatus(t *testing.T) { } func TestGetStatusAfterBlock(t *testing.T) { - t.Skip("temporary skipping for local testing, should not make it into PR") handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.WaitForBlock(c, 0) From 5edfe3eec8ba2024ca7e7183a3d7023f2c45d734 Mon Sep 17 00:00:00 2001 From: "Ryan R. Fox" Date: Wed, 3 Jun 2020 19:00:49 -0400 Subject: [PATCH 025/267] fixup goal documentation (#1134) Spelling correction --- cmd/goal/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/goal/node.go b/cmd/goal/node.go index e0f0dc4a43..69d6abafe1 100644 --- a/cmd/goal/node.go +++ b/cmd/goal/node.go @@ -110,7 +110,7 @@ var nodeCmd = &cobra.Command{ var startCmd = &cobra.Command{ Use: "start", - Short: "Inititialize the specified Algorand node.", + Short: "Initialize the specified Algorand node.", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { if !verifyPeerDialArg() { From 5f5a71635ced25e4c43d09eaa060d8f0b86af194 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 4 Jun 2020 11:19:09 -0400 Subject: [PATCH 026/267] Remove allBalances from acctupdates and move it to testing (#1135) The function acctupdates.allBalances had a functional bug where the locking semantics could cause it to work incorrectly in some cases. Fortunately, this function is not used anywhere outside our unit testings. In this PR, I've moved this function to be executed only within our unit testing, removing it's usage from ledger.AllBalances and updating relevant unit tests. --- data/common_test.go | 7 ++++--- ledger/acctupdates.go | 27 --------------------------- ledger/acctupdates_test.go | 31 +++++++++++++++++++++++++++++++ ledger/ledger.go | 7 ------- node/assemble_test.go | 5 ----- 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/data/common_test.go b/data/common_test.go index 02bd509192..0b40d4369d 100644 --- a/data/common_test.go +++ b/data/common_test.go @@ -17,6 +17,7 @@ package data import ( + "fmt" "math/rand" "strconv" "testing" @@ -125,10 +126,10 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*L tx := make([]transactions.SignedTxn, TXs) latest := ledger.Latest() - bal, err := ledger.AllBalances(latest) - if err != nil { - panic(err) + if latest != 0 { + panic(fmt.Errorf("newly created ledger doesn't start on round 0")) } + bal := bootstrap.balances for i := 0; i < TXs; i++ { send := gen.Int() % P diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 2a3a473368..6904e1697d 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -290,33 +290,6 @@ func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewa return au.accountsq.lookup(addr) } -func (au *accountUpdates) allBalances(rnd basics.Round) (bals map[basics.Address]basics.AccountData, err error) { - au.accountsMu.RLock() - - offsetLimit, err := au.roundOffset(rnd) - au.accountsMu.RUnlock() - if err != nil { - return - } - - err = au.dbs.rdb.Atomic(func(tx *sql.Tx) error { - var err0 error - bals, err0 = accountsAll(tx) - return err0 - }) - if err != nil { - return - } - au.accountsMu.RLock() - defer au.accountsMu.RUnlock() - for offset := uint64(0); offset < offsetLimit; offset++ { - for addr, delta := range au.deltas[offset] { - bals[addr] = delta.new - } - } - return -} - func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.AssetLocator, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 894dd3d165..72a5e549e0 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -17,6 +17,7 @@ package ledger import ( + "database/sql" "fmt" "runtime" "sync" @@ -90,6 +91,36 @@ func (ml *mockLedgerForTracker) trackerLog() logging.Logger { return ml.log } +// this function used to be in acctupdates.go, but we were never using it for production purposes. This +// function has a conceptual flaw in that it attempts to load the entire balances into memory. This might +// not work if we have large number of balances. On these unit testing, however, it's not the case, and it's +// safe to call it. +func (au *accountUpdates) allBalances(rnd basics.Round) (bals map[basics.Address]basics.AccountData, err error) { + au.accountsMu.RLock() + defer au.accountsMu.RUnlock() + offsetLimit, err := au.roundOffset(rnd) + + if err != nil { + return + } + + err = au.dbs.rdb.Atomic(func(tx *sql.Tx) error { + var err0 error + bals, err0 = accountsAll(tx) + return err0 + }) + if err != nil { + return + } + + for offset := uint64(0); offset < offsetLimit; offset++ { + for addr, delta := range au.deltas[offset] { + bals[addr] = delta.new + } + } + return +} + func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, latestRnd basics.Round, accts []map[basics.Address]basics.AccountData, rewards []uint64, proto config.ConsensusParams) { latest := au.latest() require.Equal(t, latest, latestRnd) diff --git a/ledger/ledger.go b/ledger/ledger.go index e0e1a4df3a..e5bb322fe4 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -500,13 +500,6 @@ func (l *Ledger) Timestamp(r basics.Round) (int64, error) { return l.time.timestamp(r) } -// AllBalances returns a map of every account balance as of round rnd. -func (l *Ledger) AllBalances(rnd basics.Round) (map[basics.Address]basics.AccountData, error) { - l.trackerMu.RLock() - defer l.trackerMu.RUnlock() - return l.accts.allBalances(rnd) -} - // GenesisHash returns the genesis hash for this ledger. func (l *Ledger) GenesisHash() crypto.Digest { return l.genesisHash diff --git a/node/assemble_test.go b/node/assemble_test.go index 5821af1d5d..53fc09a20d 100644 --- a/node/assemble_test.go +++ b/node/assemble_test.go @@ -86,11 +86,6 @@ func BenchmarkAssembleBlock(b *testing.B) { require.NoError(b, err) l := ledger - // allb, err := l.AllBalances(l.Latest()) - // require.NoError(b, err) - // for addr, ad := range allb { - // b.Logf("%s\t%d", addr, ad.MicroAlgos) - // } next := l.LastRound() if err != nil { b.Errorf("could not make proposals at round %d: could not read block from ledger: %v", next, err) From a90ad7be1ecb075ec1e7ed9e23e352a5b1cbb25c Mon Sep 17 00:00:00 2001 From: btoll Date: Thu, 4 Jun 2020 12:01:05 -0400 Subject: [PATCH 027/267] Add ability to create an image for betanet (#1124) Add the ability to create a docker image for betanet. --- docker/releases/build_releases.sh | 38 +++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/docker/releases/build_releases.sh b/docker/releases/build_releases.sh index 6d0e0ea672..33967d5fcb 100755 --- a/docker/releases/build_releases.sh +++ b/docker/releases/build_releases.sh @@ -5,16 +5,26 @@ # Login name is "algorand". # # To build and push to docker hub the latest release: +# +# For mainnet: # ./build_releases.sh # ./build_releases.sh --tagname 2.0.6 +# +# For testnet: +# ./build_releases.sh --network testnet +# +# For betanet: +# ./build_releases.sh --network betanet +# GREEN_FG=$(tput setaf 2 2>/dev/null) RED_FG=$(tput setaf 1 2>/dev/null) END_FG_COLOR=$(tput sgr0 2>/dev/null) # These are reasonable defaults. +CHANNEL=stable DEPLOY=true -NAME=stable +IMAGE_NAME=stable NETWORK=mainnet TAGNAME=latest @@ -22,7 +32,7 @@ while [ "$1" != "" ]; do case "$1" in --name) shift - NAME="${1-stable}" + IMAGE_NAME="${1-stable}" ;; --network) shift @@ -43,15 +53,19 @@ while [ "$1" != "" ]; do shift done -if [[ ! "$NETWORK" =~ ^mainnet$|^testnet$ ]] +if [[ ! "$NETWORK" =~ ^mainnet$|^testnet$|^betanet$ ]] then - echo "$RED_FG[$0]$END_FG_COLOR Network values must be either \`mainnet\` or \`testnet\`." + echo "$RED_FG[$0]$END_FG_COLOR Network values must be either \`mainnet\`, \`testnet\` or \`betanet\`." exit 1 -fi - -if [ "$NAME" == "testnet" ] +elif [ "$NETWORK" != "mainnet" ] then - NETWORK="-g $1" + if [ "$NETWORK" == "betanet" ] + then + CHANNEL=beta + fi + + IMAGE_NAME="$NETWORK" + NETWORK="-g $NETWORK" fi build_image () { @@ -60,13 +74,13 @@ build_image () { RUN apt-get update && apt-get install -y ca-certificates curl --no-install-recommends && \ curl --silent -L https://github.com/algorand/go-algorand-doc/blob/master/downloads/installers/linux_amd64/install_master_linux-amd64.tar.gz?raw=true | tar xzf - && \ - ./update.sh -c stable -n -p ~/node -d ~/node/data -i $NETWORK + ./update.sh -c $CHANNEL -n -p ~/node -d ~/node/data -i $NETWORK WORKDIR /root/node EOF - if ! echo "$DOCKERFILE" | docker build -t "algorand/$NAME:$TAGNAME" - + if ! echo "$DOCKERFILE" | docker build -t "algorand/$IMAGE_NAME:$TAGNAME" - then - echo -e "\n$RED_FG[$0]$END_FG_COLOR The algorand/$NAME:$TAGNAME image could not be built." + echo -e "\n$RED_FG[$0]$END_FG_COLOR The algorand/$IMAGE_NAME:$TAGNAME image could not be built." exit 1 fi } @@ -75,7 +89,7 @@ build_image if $DEPLOY then - if ! docker push "algorand/$NAME:$TAGNAME" + if ! docker push "algorand/$IMAGE_NAME:$TAGNAME" then echo -e "\n$RED_FG[$0]$END_FG_COLOR \`docker push\` failed." exit 1 From bffcfc28eed1c255975435da214b435f7a83ab96 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 5 Jun 2020 12:42:13 -0400 Subject: [PATCH 028/267] Add EnableGossipBlockService to control enabling of gossip block service. (#1137) Previous release disabled the block servicing on nodes, as node doesn't expose the gossip http entrypoint. However, the gossip block service was also disabled, which wasn't the original intent. This PR adds a separate config option, which default to enabling the gossip block service. --- config/config.go | 4 ++ config/config_test.go | 7 ++- config/local_defaults.go | 73 +++++++++++++++++++++++++++- installer/config.json.example | 3 +- netdeploy/network.go | 9 ++-- rpcs/blockService.go | 26 +++++----- test/testdata/configs/config-v8.json | 54 ++++++++++++++++++++ 7 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 test/testdata/configs/config-v8.json diff --git a/config/config.go b/config/config.go index 463f5d0bed..afe5f21879 100644 --- a/config/config.go +++ b/config/config.go @@ -288,6 +288,10 @@ type Local struct { // EnableBlockService enables the block serving service. The functionality of this depends on NetAddress, which must also be provided. // This functionality is required for the catchup. EnableBlockService bool + + // EnableGossipBlockService enables the block serving service over the gossip network. The functionality of this depends on NetAddress, which must also be provided. + // This functionality is required for the relays to perform catchup from nodes. + EnableGossipBlockService bool } // Filenames of config files within the configdir (e.g. ~/.algorand) diff --git a/config/config_test.go b/config/config_test.go index 7d1de545db..7f89a8fb38 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -314,7 +314,7 @@ func TestConfigMigrateFromDisk(t *testing.T) { func TestConfigInvariant(t *testing.T) { a := require.New(t) - a.Equal(uint32(7), configVersion, "If you bump Config Version, please update this test (and consider if you should be adding more)") + a.Equal(uint32(8), configVersion, "If you bump Config Version, please update this test (and consider if you should be adding more)") ourPath, err := os.Getwd() a.NoError(err) @@ -359,6 +359,11 @@ func TestConfigInvariant(t *testing.T) { err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v7.json"), &c7) a.NoError(err) a.Equal(defaultLocalV7, c7) + + c8 := Local{} + err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v8.json"), &c8) + a.NoError(err) + a.Equal(defaultLocalV8, c8) } func TestConfigLatestVersion(t *testing.T) { diff --git a/config/local_defaults.go b/config/local_defaults.go index 12e5d0c59d..bf65ad1244 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -21,9 +21,9 @@ import ( "time" ) -var defaultLocal = defaultLocalV7 +var defaultLocal = defaultLocalV8 -const configVersion = uint32(7) +const configVersion = uint32(8) // !!! WARNING !!! // @@ -39,6 +39,66 @@ const configVersion = uint32(7) // // !!! WARNING !!! +var defaultLocalV8 = Local{ + // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file + Version: 8, + Archival: false, + BaseLoggerDebugLevel: 4, + BroadcastConnectionsLimit: -1, + AnnounceParticipationKey: true, + PriorityPeers: map[string]bool{}, + CadaverSizeTarget: 1073741824, + CatchupFailurePeerRefreshRate: 10, + CatchupParallelBlocks: 16, + ConnectionsRateLimitingCount: 60, + ConnectionsRateLimitingWindowSeconds: 1, + DeadlockDetection: 0, + DNSBootstrapID: ".algorand.network", + EnableAgreementReporting: false, + EnableAgreementTimeMetrics: false, + EnableIncomingMessageFilter: false, + EnableMetricReporting: false, + EnableOutgoingNetworkMessageFiltering: true, + EnableRequestLogger: false, + EnableTopAccountsReporting: false, + EndpointAddress: "127.0.0.1:0", + GossipFanout: 4, + IncomingConnectionsLimit: 10000, + IncomingMessageFilterBucketCount: 5, + IncomingMessageFilterBucketSize: 512, + LogArchiveName: "node.archive.log", + LogArchiveMaxAge: "", + LogSizeLimit: 1073741824, + MaxConnectionsPerIP: 30, + NetAddress: "", + NodeExporterListenAddress: ":9100", + NodeExporterPath: "./node_exporter", + OutgoingMessageFilterBucketCount: 3, + OutgoingMessageFilterBucketSize: 128, + ReconnectTime: 1 * time.Minute, + ReservedFDs: 256, + RestReadTimeoutSeconds: 15, + RestWriteTimeoutSeconds: 120, + RunHosted: false, + SuggestedFeeBlockHistory: 3, + SuggestedFeeSlidingWindowSize: 50, + TelemetryToLog: true, + TxPoolExponentialIncreaseFactor: 2, + TxPoolSize: 15000, + TxSyncIntervalSeconds: 60, + TxSyncTimeoutSeconds: 30, + TxSyncServeResponseSize: 1000000, + PeerConnectionsUpdateInterval: 3600, + DNSSecurityFlags: 0x01, + EnablePingHandler: true, + CatchpointInterval: 10000, + CatchpointFileHistoryLength: 365, + EnableLedgerService: false, + EnableBlockService: false, + EnableGossipBlockService: true, // added in V8 + // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file +} + var defaultLocalV7 = Local{ // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file Version: 7, @@ -497,6 +557,15 @@ func migrate(cfg Local) (newCfg Local, err error) { newCfg.Version = 7 } + // Migrate 7 -> 8 + if newCfg.Version == 7 { + if newCfg.EnableGossipBlockService == defaultLocalV7.EnableGossipBlockService { + newCfg.EnableGossipBlockService = defaultLocalV8.EnableGossipBlockService + } + + newCfg.Version = 8 + } + if newCfg.Version != configVersion { err = fmt.Errorf("failed to migrate config version %d (stuck at %d) to latest %d", cfg.Version, newCfg.Version, configVersion) } diff --git a/installer/config.json.example b/installer/config.json.example index b707688517..1df169add2 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -1,5 +1,5 @@ { - "Version": 7, + "Version": 8, "AnnounceParticipationKey": true, "Archival": false, "BaseLoggerDebugLevel": 4, @@ -14,6 +14,7 @@ "DisableOutgoingConnectionThrottling": false, "DeadlockDetection": 0, "DNSBootstrapID": ".algorand.network", + "EnableGossipBlockService": true, "EnableIncomingMessageFilter": false, "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, diff --git a/netdeploy/network.go b/netdeploy/network.go index 8d268dacac..68f23177da 100644 --- a/netdeploy/network.go +++ b/netdeploy/network.go @@ -307,9 +307,13 @@ func (n Network) GetPeerAddresses(binDir string) []string { for _, relayDir := range n.cfg.RelayDirs { nc := nodecontrol.MakeNodeController(binDir, n.getNodeFullPath(relayDir)) relayAddress, err := nc.GetListeningAddress() - if err == nil { - peerAddresses = append(peerAddresses, relayAddress) + if err != nil { + continue } + if strings.HasPrefix(relayAddress, "http://") { + relayAddress = relayAddress[7:] + } + peerAddresses = append(peerAddresses, relayAddress) } return peerAddresses } @@ -336,7 +340,6 @@ func (n Network) StartNode(binDir, nodeDir string, redirectOutput bool) (err err controller := nodecontrol.MakeNodeController(binDir, nodeDir) peers := n.GetPeerAddresses(binDir) peerAddresses := strings.Join(peers, ";") - _, err = controller.StartAlgod(nodecontrol.AlgodStartArgs{ PeerAddress: peerAddresses, RedirectOutput: redirectOutput, diff --git a/rpcs/blockService.go b/rpcs/blockService.go index 53b772a2be..a2a08917e2 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -50,12 +50,13 @@ const BlockServiceBlockPath = "/v{version:[0-9.]+}/{genesisID}/block/{round:[0-9 // BlockService represents the Block RPC API type BlockService struct { - ledger *data.Ledger - genesisID string - catchupReqs chan network.IncomingMessage - stop chan struct{} - net network.GossipNode - enableService bool + ledger *data.Ledger + genesisID string + catchupReqs chan network.IncomingMessage + stop chan struct{} + net network.GossipNode + enableService bool + enableServiceOverGossip bool } // EncodedBlockCert defines how GetBlockBytes encodes a block and its certificate @@ -77,11 +78,12 @@ type PreEncodedBlockCert struct { // MakeBlockService creates a BlockService around the provider Ledger and registers it for HTTP callback on the block serving path func MakeBlockService(config config.Local, ledger *data.Ledger, net network.GossipNode, genesisID string) *BlockService { service := &BlockService{ - ledger: ledger, - genesisID: genesisID, - catchupReqs: make(chan network.IncomingMessage, config.CatchupParallelBlocks*blockServerCatchupRequestBufferSize), - net: net, - enableService: config.EnableBlockService, + ledger: ledger, + genesisID: genesisID, + catchupReqs: make(chan network.IncomingMessage, config.CatchupParallelBlocks*blockServerCatchupRequestBufferSize), + net: net, + enableService: config.EnableBlockService, + enableServiceOverGossip: config.EnableGossipBlockService, } if service.enableService { net.RegisterHTTPHandler(BlockServiceBlockPath, service) @@ -91,7 +93,7 @@ func MakeBlockService(config config.Local, ledger *data.Ledger, net network.Goss // Start listening to catchup requests over ws func (bs *BlockService) Start() { - if bs.enableService { + if bs.enableServiceOverGossip { handlers := []network.TaggedMessageHandler{ {Tag: protocol.UniCatchupReqTag, MessageHandler: network.HandlerFunc(bs.processIncomingMessage)}, {Tag: protocol.UniEnsBlockReqTag, MessageHandler: network.HandlerFunc(bs.processIncomingMessage)}, diff --git a/test/testdata/configs/config-v8.json b/test/testdata/configs/config-v8.json new file mode 100644 index 0000000000..498412b0f8 --- /dev/null +++ b/test/testdata/configs/config-v8.json @@ -0,0 +1,54 @@ +{ + "Version": 8, + "AnnounceParticipationKey": true, + "Archival": false, + "BaseLoggerDebugLevel": 4, + "BroadcastConnectionsLimit": -1, + "CadaverSizeTarget": 1073741824, + "CatchupFailurePeerRefreshRate": 10, + "CatchupParallelBlocks": 16, + "CatchpointInterval": 10000, + "CatchpointFileHistoryLength": 365, + "ConnectionsRateLimitingWindowSeconds": 1, + "ConnectionsRateLimitingCount": 60, + "DeadlockDetection": 0, + "DNSBootstrapID": ".algorand.network", + "DNSSecurityFlags": 1, + "EnableAgreementReporting": false, + "EnableGossipBlockService": true, + "EnableIncomingMessageFilter": false, + "EnableMetricReporting": false, + "EnableOutgoingNetworkMessageFiltering": true, + "EnablePingHandler": true, + "EnableRequestLogger": false, + "EnableTopAccountsReporting": false, + "EndpointAddress": "127.0.0.1:0", + "GossipFanout": 4, + "IncomingConnectionsLimit": 10000, + "IncomingMessageFilterBucketCount": 5, + "IncomingMessageFilterBucketSize": 512, + "LogArchiveMaxAge": "", + "LogArchiveName": "node.archive.log", + "LogSizeLimit": 1073741824, + "MaxConnectionsPerIP": 30, + "NetAddress": "", + "NodeExporterListenAddress": ":9100", + "NodeExporterPath": "./node_exporter", + "OutgoingMessageFilterBucketCount": 3, + "OutgoingMessageFilterBucketSize": 128, + "PriorityPeers": {}, + "ReconnectTime": 60000000000, + "ReservedFDs": 256, + "RestReadTimeoutSeconds": 15, + "RestWriteTimeoutSeconds": 120, + "RunHosted": false, + "SuggestedFeeBlockHistory": 3, + "TelemetryToLog": true, + "TxPoolExponentialIncreaseFactor": 2, + "TxPoolSize": 15000, + "TxSyncIntervalSeconds": 60, + "TxSyncTimeoutSeconds": 30, + "TxSyncServeResponseSize": 1000000, + "SuggestedFeeSlidingWindowSize": 50, + "PeerConnectionsUpdateInterval": 3600 +} From 6236cf10857349dfb771bf914a550bff6089259c Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 5 Jun 2020 17:22:11 -0400 Subject: [PATCH 029/267] Fix incorrect asset error message when trying to overspent an asset. (#1142) --- data/transactions/asset.go | 4 ++-- test/e2e-go/features/transactions/asset_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/transactions/asset.go b/data/transactions/asset.go index 99b8870194..ce998c9541 100644 --- a/data/transactions/asset.go +++ b/data/transactions/asset.go @@ -213,11 +213,11 @@ func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, am return fmt.Errorf("asset %v frozen in %v", asset, addr) } - var overflowed bool - sndHolding.Amount, overflowed = basics.OSub(sndHolding.Amount, amount) + newAmount, overflowed := basics.OSub(sndHolding.Amount, amount) if overflowed { return fmt.Errorf("underflow on subtracting %d from sender amount %d", amount, sndHolding.Amount) } + sndHolding.Amount = newAmount snd.Assets[asset] = sndHolding return balances.Put(snd) diff --git a/test/e2e-go/features/transactions/asset_test.go b/test/e2e-go/features/transactions/asset_test.go index 715fa73062..68a2659e52 100644 --- a/test/e2e-go/features/transactions/asset_test.go +++ b/test/e2e-go/features/transactions/asset_test.go @@ -792,13 +792,13 @@ func TestAssetSend(t *testing.T) { tx, err = client.MakeUnsignedAssetSendTx(nonFrozenIdx, 11, extra, "", "") _, err = helperFillSignBroadcast(client, wh, extra, tx, err) a.Error(err) - a.True(strings.Contains(err.Error(), "underflow on subtracting 11 from sender amount")) + a.True(strings.Contains(err.Error(), "underflow on subtracting 11 from sender amount 10")) // Should not be able to clawback more than is available tx, err = client.MakeUnsignedAssetSendTx(nonFrozenIdx, 11, account0, "", extra) _, err = helperFillSignBroadcast(client, wh, clawback, tx, err) a.Error(err) - a.True(strings.Contains(err.Error(), "underflow on subtracting 11 from sender amount")) + a.True(strings.Contains(err.Error(), "underflow on subtracting 11 from sender amount 10")) tx, err = client.MakeUnsignedAssetSendTx(nonFrozenIdx, 10, extra, "", "") _, err = helperFillSignBroadcast(client, wh, extra, tx, err) From c6b8aee6c60726d3a262a84586ba1cc608d4bc49 Mon Sep 17 00:00:00 2001 From: Russ Fustino Date: Sat, 6 Jun 2020 15:36:02 -0400 Subject: [PATCH 030/267] Parameter support for dispenser (#1053) This feature will allow for a more seemless integrtion with Mobile Apps and Websties that use TestNet and BetaNet Dispensers --- cmd/dispenser/server.go | 54 ++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/cmd/dispenser/server.go b/cmd/dispenser/server.go index a4d2203e73..3f766b5994 100644 --- a/cmd/dispenser/server.go +++ b/cmd/dispenser/server.go @@ -67,44 +67,48 @@ const topPageTemplate = ` Algorand dispenser - - - + + -

Algorand dispenser

- -
- +
+
-
- Status: + Status:
From 991c9684d0d2400fe11d68d5a63ee1eadb18e5e5 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 6 Jun 2020 18:44:39 -0400 Subject: [PATCH 031/267] Improve the logic of the TestCatchupOverGossip (#1144) The TestCatchupOverGossip was randomly failing during testing on slow machines. The culprit was that we were making few performance related assumptions, which were not correct. In this case, we started a network, and shut it right away. The assumption was that the network would not be able to make any progress since it was shut down right away. This is not guaranteed to be the case. The better way to make sure no progress is made on a node is to prevent it from running at all. --- .../features/catchup/basicCatchup_test.go | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/test/e2e-go/features/catchup/basicCatchup_test.go b/test/e2e-go/features/catchup/basicCatchup_test.go index 5e7c15b8e5..2a414ce510 100644 --- a/test/e2e-go/features/catchup/basicCatchup_test.go +++ b/test/e2e-go/features/catchup/basicCatchup_test.go @@ -124,35 +124,29 @@ func runCatchupOverGossip(t *testing.T, cfg.SaveToDisk(dir) } - fixture.Start() defer fixture.Shutdown() ncPrim, err := fixture.GetNodeController("Primary") a.NoError(err) - // Kill the primary - ncPrim.FullStop() - // Get 2nd node, which makes all the progress nc, err := fixture.GetNodeController("Node") a.NoError(err) - // Let the network make some progress + // Start the secondary + _, err = fixture.StartNode(nc.GetDataDir()) + a.NoError(err) + // Let the secondary make progress up to round 3, while the primary was never startred ( hence, it's on round = 0) waitForRound := uint64(3) err = fixture.ClientWaitForRoundWithTimeout(fixture.GetAlgodClientForController(nc), waitForRound) a.NoError(err) - // Now, revive the primary - lg, err := fixture.StartNode(ncPrim.GetDataDir()) - a.NoError(err) - - status, err := lg.Status() - a.NoError(err) - a.True(status.LastRound < waitForRound) - - // Now, kill the secondary and restart it to reinitiate inbound connection + // stop the secondary, which is on round 3 or more. nc.FullStop() - _, err = fixture.StartNode(nc.GetDataDir()) + + // Now, start both primary and secondary, and let the primary catchup up. + fixture.Start() + lg, err := fixture.StartNode(ncPrim.GetDataDir()) a.NoError(err) // Now, catch up From 4ee1485e920c18e5a788596d7150b108809ffc20 Mon Sep 17 00:00:00 2001 From: btoll Date: Mon, 8 Jun 2020 11:29:06 -0400 Subject: [PATCH 032/267] Change staging location to algorand-staging (#1136) Change the location of staging from algorand-builds to algorand-staging to be in sync with the new mule tasks. --- scripts/release/build/stage/upload/run.sh | 2 +- scripts/release/prod/stage/snapshot.sh | 2 +- scripts/release/prod/stage/sync/run.sh | 2 +- scripts/release/test/stage/setup/run.sh | 3 +-- scripts/release/test/stage/test/run.sh | 2 +- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/release/build/stage/upload/run.sh b/scripts/release/build/stage/upload/run.sh index b69c715603..54db9f4e80 100755 --- a/scripts/release/build/stage/upload/run.sh +++ b/scripts/release/build/stage/upload/run.sh @@ -15,5 +15,5 @@ rm -rf pkg && mkdir -p pkg/"$FULLVERSION" ssh -i ReleaseBuildInstanceKey.pem -A ubuntu@"$INSTANCE" bash go/src/github.com/algorand/go-algorand/scripts/release/build/stage/upload/task.sh scp -i ReleaseBuildInstanceKey.pem -o StrictHostKeyChecking=no -r ubuntu@"$INSTANCE":~/node_pkg/* pkg/"$FULLVERSION"/ -aws s3 sync --exclude dev* --exclude master* --exclude nightly* --exclude stable* pkg/"$FULLVERSION" s3://algorand-builds/channel/"$CHANNEL"/ +aws s3 sync --exclude dev* --exclude master* --exclude nightly* --exclude stable* pkg/"$FULLVERSION" "s3://algorand-staging/releases/$CHANNEL/$FULLVERSION/" diff --git a/scripts/release/prod/stage/snapshot.sh b/scripts/release/prod/stage/snapshot.sh index 1fd5c19576..1a2cc0d783 100755 --- a/scripts/release/prod/stage/snapshot.sh +++ b/scripts/release/prod/stage/snapshot.sh @@ -17,7 +17,7 @@ echo DEBS_DIR="$HOME/packages/deb/$CHANNEL" DEB="algorand_${CHANNEL}_linux-amd64_${VERSION}.deb" -aws s3 cp "s3://algorand-builds/channel/$CHANNEL/$DEB" . +aws s3 cp "s3://algorand-staging/releases/$CHANNEL/$VERSION/$DEB" . mv "$DEB" "$DEBS_DIR" aptly repo add algorand "$DEBS_DIR" diff --git a/scripts/release/prod/stage/sync/run.sh b/scripts/release/prod/stage/sync/run.sh index 555a0058e9..34efeeb966 100755 --- a/scripts/release/prod/stage/sync/run.sh +++ b/scripts/release/prod/stage/sync/run.sh @@ -12,7 +12,7 @@ fi RSTAMP=$(./scripts/release/prod/reverse_hex_timestamp) -if ! aws s3 sync --exclude="*" --include="*$VERSION*" "s3://algorand-builds/channel/$CHANNEL" "s3://algorand-dev-deb-repo/releases/$CHANNEL/${RSTAMP}_${VERSION}"; then +if ! aws s3 sync --exclude="*" --include="*$VERSION*" "s3://algorand-staging/releases/$CHANNEL/$VERSION/" "s3://algorand-dev-deb-repo/releases/$CHANNEL/${RSTAMP}_${VERSION}"; then echo There was a problem syncing the staging and production buckets! exit 1 fi diff --git a/scripts/release/test/stage/setup/run.sh b/scripts/release/test/stage/setup/run.sh index 5ff49a03ce..4aef7cce39 100755 --- a/scripts/release/test/stage/setup/run.sh +++ b/scripts/release/test/stage/setup/run.sh @@ -21,8 +21,7 @@ RELEASE=$(sed -n 's/.*FULLVERSION=\(.*\)/\1/p' <<< "$BUILD_ENV") rm -rf pkg/* && mkdir -p pkg/"$FULLVERSION" -#aws s3 sync s3://"$BUCKET"/"$CHANNEL"/"$RELEASE" pkg/ --exclude "*" --include "*.deb" --include "*.rpm" -aws s3 sync s3://algorand-builds/channel/"$CHANNEL"/"$RELEASE" pkg/ --exclude "*" --include "*.deb" --include "*.rpm" +aws s3 sync "s3://algorand-staging/releases/$CHANNEL/$RELEASE/" pkg/ --exclude "*" --include "*.deb" --include "*.rpm" # Upload the packages and their signatures. scp -i ReleaseBuildInstanceKey.pem -o StrictHostKeyChecking=no -r pkg/* ubuntu@"$INSTANCE":~/node_pkg/ diff --git a/scripts/release/test/stage/test/run.sh b/scripts/release/test/stage/test/run.sh index 7e416caa53..9b4b8e8201 100755 --- a/scripts/release/test/stage/test/run.sh +++ b/scripts/release/test/stage/test/run.sh @@ -12,7 +12,7 @@ CHANNEL=$(sed -n 's/.*CHANNEL=\(.*\)/\1/p' <<< "$BUILD_ENV") RELEASE=$(sed -n 's/.*FULLVERSION=\(.*\)/\1/p' <<< "$BUILD_ENV") rm -rf ./*.deb ./*.rpm -python3 scripts/get_current_installers.py "s3://algorand-builds/channel/$CHANNEL/$RELEASE" +python3 scripts/get_current_installers.py "s3://algorand-staging/releases/$CHANNEL/$RELEASE" # Copy previous installers into ~. scp -i ReleaseBuildInstanceKey.pem -o StrictHostKeyChecking=no ./*.deb ubuntu@"$INSTANCE": From 7acdb2cd78c935a328f76889f63078b1768d3f2f Mon Sep 17 00:00:00 2001 From: Derek Leung Date: Mon, 8 Jun 2020 12:56:21 -0400 Subject: [PATCH 033/267] Run make fmt (#1145) Run make fmt on the project. --- cmd/kmd/main.go | 4 ++-- data/committee/credential.go | 2 +- logging/telemetryConfig.go | 4 ++-- logging/telemetryConfig_test.go | 4 ++-- network/requestTracker_test.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/kmd/main.go b/cmd/kmd/main.go index 27cf2f7202..d08326f11d 100644 --- a/cmd/kmd/main.go +++ b/cmd/kmd/main.go @@ -23,9 +23,9 @@ import ( "path/filepath" "time" - "golang.org/x/sys/unix" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" + "golang.org/x/sys/unix" "github.com/algorand/go-algorand/cmd/kmd/codes" "github.com/algorand/go-algorand/daemon/kmd" @@ -52,7 +52,7 @@ func init() { var kmdCmd = &cobra.Command{ Use: "kmd", Short: "Key Management Daemon (kmd)", - Long: `The Key Management Daemon (kmd) is a low level wallet and key management + Long: `The Key Management Daemon (kmd) is a low level wallet and key management tool. It works in conjunction with algod and goal to keep secrets safe. An optional timeout flag will automatically terminate kmd after a number of seconds has elapsed, allowing a simple way to ensure kmd will be shutdown in diff --git a/data/committee/credential.go b/data/committee/credential.go index fc9abb8cc8..df33e62035 100644 --- a/data/committee/credential.go +++ b/data/committee/credential.go @@ -254,7 +254,7 @@ func (cred Credential) LowestOutputDigest() crypto.Digest { if len(lbytes) > len(out) { panic("Cred lowest output too long") } - copy(out[len(out) - len(lbytes):], lbytes) + copy(out[len(out)-len(lbytes):], lbytes) return out } diff --git a/logging/telemetryConfig.go b/logging/telemetryConfig.go index 062159f458..a127d91833 100644 --- a/logging/telemetryConfig.go +++ b/logging/telemetryConfig.go @@ -65,8 +65,8 @@ func createTelemetryConfig() TelemetryConfig { MinLogLevel: logrus.WarnLevel, ReportHistoryLevel: logrus.WarnLevel, // These credentials are here intentionally. Not a bug. - UserName: "telemetry-v9", - Password: "oq%$FA1TOJ!yYeMEcJ7D688eEOE#MGCu", + UserName: "telemetry-v9", + Password: "oq%$FA1TOJ!yYeMEcJ7D688eEOE#MGCu", } } diff --git a/logging/telemetryConfig_test.go b/logging/telemetryConfig_test.go index a30abdff50..c1c74622f7 100644 --- a/logging/telemetryConfig_test.go +++ b/logging/telemetryConfig_test.go @@ -34,8 +34,8 @@ func Test_loadTelemetryConfig(t *testing.T) { MinLogLevel: 4, ReportHistoryLevel: 4, // These credentials are here intentionally. Not a bug. - UserName: "telemetry-v9", - Password: "oq%$FA1TOJ!yYeMEcJ7D688eEOE#MGCu", + UserName: "telemetry-v9", + Password: "oq%$FA1TOJ!yYeMEcJ7D688eEOE#MGCu", } a := require.New(t) diff --git a/network/requestTracker_test.go b/network/requestTracker_test.go index 2f8f1957a6..d0169c34ef 100644 --- a/network/requestTracker_test.go +++ b/network/requestTracker_test.go @@ -78,7 +78,7 @@ func TestRateLimiting(t *testing.T) { wn := &WebsocketNetwork{ log: log, config: defaultConfig, - phonebook: MakePhonebook(1,1), + phonebook: MakePhonebook(1, 1), GenesisID: "go-test-network-genesis", NetworkID: config.Devtestnet, } From 04fd1f45294a0b22f4815dd08982c8e8e162737e Mon Sep 17 00:00:00 2001 From: algonautshant <55754073+algonautshant@users.noreply.github.com> Date: Mon, 8 Jun 2020 18:22:33 -0400 Subject: [PATCH 034/267] Blank reseve address shows creator's address. (#1107) The reserve address, when blank, is shown in the asset info as if it is set to the creator's address. When reserve address change transaction is accepted, nothing changes (because the blank remains blank, and shows the creator's address). When the reserve address is empty, but showing the creator's address, then it does not count towards a non-empty address. So, when all the addresses but the manager are empty, but shows as if reserve and manager are non-empty, clearing the manager address destroys the asset. This change removes the lines which set the reserve address to the creator's address when the reserve address is blank. --- cmd/goal/asset.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/goal/asset.go b/cmd/goal/asset.go index a31557fef6..3d9de1fe95 100644 --- a/cmd/goal/asset.go +++ b/cmd/goal/asset.go @@ -620,7 +620,9 @@ var infoAssetCmd = &cobra.Command{ reportErrorf(errorRequestFail, err) } + reserveEmpty := false if params.ReserveAddr == "" { + reserveEmpty = true params.ReserveAddr = params.Creator } @@ -641,7 +643,11 @@ var infoAssetCmd = &cobra.Command{ fmt.Printf("Decimals: %d\n", params.Decimals) fmt.Printf("Default frozen: %v\n", params.DefaultFrozen) fmt.Printf("Manager address: %s\n", params.ManagerAddr) - fmt.Printf("Reserve address: %s\n", params.ReserveAddr) + if reserveEmpty { + fmt.Printf("Reserve address: %s (Empty. Defaulting to creator)\n", params.ReserveAddr) + } else { + fmt.Printf("Reserve address: %s\n", params.ReserveAddr) + } fmt.Printf("Freeze address: %s\n", params.FreezeAddr) fmt.Printf("Clawback address: %s\n", params.ClawbackAddr) }, From bfb3df956784ec38cebd791db80cdd64cf7b9b45 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 8 Jun 2020 20:50:48 -0400 Subject: [PATCH 035/267] Add license headers to dbgen generated files (#1143) Since we added the headers to all our go files, we had to maintain them to have these headers. That didn't take into account auto-generated go files, such as agreeInstall.go. This file is generated and when it does, the license header is missing. To correct this, I've added the license header to the generator. --- agreement/persistence.go | 1 - agreement/service.go | 2 +- cmd/dbgen/main.go | 24 +++++++++++++++++++----- data/account/account.go | 3 +-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/agreement/persistence.go b/agreement/persistence.go index ff20602a1a..c6cc4c99f6 100644 --- a/agreement/persistence.go +++ b/agreement/persistence.go @@ -16,7 +16,6 @@ package agreement -//go:generate dbgen -i agree.sql -p agreement -n agree -o agreeInstall.go import ( "context" "database/sql" diff --git a/agreement/service.go b/agreement/service.go index 6fc470c030..9289bcf575 100644 --- a/agreement/service.go +++ b/agreement/service.go @@ -16,7 +16,7 @@ package agreement -//go:generate dbgen -i agree.sql -p agreement -n agree -o agreeInstall.go +//go:generate dbgen -i agree.sql -p agreement -n agree -o agreeInstall.go -h ../scripts/LICENSE_HEADER import ( "context" "time" diff --git a/cmd/dbgen/main.go b/cmd/dbgen/main.go index c1002affaa..6a6f0c7024 100644 --- a/cmd/dbgen/main.go +++ b/cmd/dbgen/main.go @@ -18,16 +18,21 @@ // for a sqlite3 database schema installation. package main -import "fmt" -import "flag" -import "io/ioutil" +import ( + "flag" + "fmt" + "io/ioutil" + "strings" + "time" +) var inputfilename = flag.String("i", "", "Name of the file specifying the database schema.") var outputfilename = flag.String("o", "", "Name of the file to write out the resulting output. Default is stdout.") var fnname = flag.String("n", "", "Name of the desired installation function; signature is \"func InstallDatabase(*sql.Tx) error\"") var pkgname = flag.String("p", "", "Name of the package to place installation function in") +var headerfilename = flag.String("h", "", "Name of the header file to use") -var template = `// generated by dbgen; DO NOT EDIT +var template = `%s// generated by dbgen; DO NOT EDIT package %s @@ -58,8 +63,17 @@ func main() { if err != nil { panic(err) } + header := "" + if *headerfilename != "" { + headerBytes, err := ioutil.ReadFile(*headerfilename) + if err != nil { + panic(err) + } + header = string(headerBytes) + "\n" + header = strings.Replace(header, "{DATE_Y}", fmt.Sprintf("%d", time.Now().Year()), 1) + } - payload := fmt.Sprintf(template, *pkgname, *fnname, string(input)) + payload := fmt.Sprintf(template, header, *pkgname, *fnname, string(input)) if *outputfilename == "" { fmt.Println(payload) diff --git a/data/account/account.go b/data/account/account.go index a61c64630d..7e3f877448 100644 --- a/data/account/account.go +++ b/data/account/account.go @@ -16,8 +16,7 @@ package account -//go:generate dbgen -i root.sql -p account -n root -o rootInstall.go -//go:generate dbgen -i part.sql -p account -n part -o partInstall.go +//go:generate dbgen -i root.sql -p account -n root -o rootInstall.go -h ../../scripts/LICENSE_HEADER import ( "database/sql" "fmt" From 4cb8ecebde20b957b277c7a4cf2a4ae24438b87c Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 9 Jun 2020 09:08:26 -0400 Subject: [PATCH 036/267] Fix expectation in GetStatusAfterBlock; document reason we are testing 400response rather than 200response --- daemon/algod/api/server/v2/test/handlers_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 08180208fd..22b7db92cc 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -134,7 +134,9 @@ func TestGetStatusAfterBlock(t *testing.T) { defer releasefunc() err := handler.WaitForBlock(c, 0) require.NoError(t, err) - require.Equal(t, 200, rec.Code) + // Expect 400 - the test ledger will always cause "errRequestedRoundInUnsupportedRound", + // as it has not participated in agreement to build blockheaders + require.Equal(t, 400, rec.Code) } func TestGetTransactionParams(t *testing.T) { From b3275155a2d7151001aeec2fed744e55a292b348 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Tue, 9 Jun 2020 10:24:01 -0400 Subject: [PATCH 037/267] Change testingenv per changes in master to Ledger --- daemon/algod/api/server/v2/test/helpers.go | 13 +++++++++---- data/genesisBalances.go | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index ef728d42ac..7566a36fe8 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -192,6 +192,7 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d for _, acc := range accessors { acc.Close() } + } // generate accounts @@ -244,17 +245,19 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d // generate test transactions const inMem = true - ledger, err := data.LoadLedger(logging.Base(), t.Name(), inMem, protocol.ConsensusCurrentVersion, bootstrap, genesisID, genesisHash, nil, config.GetDefaultLocal()) + cfg := config.GetDefaultLocal() + cfg.Archival = true + ledger, err := data.LoadLedger(logging.Base(), t.Name(), inMem, protocol.ConsensusCurrentVersion, bootstrap, genesisID, genesisHash, nil, cfg) if err != nil { panic(err) } tx := make([]transactions.SignedTxn, TXs) latest := ledger.Latest() - bal, err := ledger.AllBalances(latest) - if err != nil { - panic(err) + if latest != 0 { + panic(fmt.Errorf("newly created ledger doesn't start on round 0")) } + bal := bootstrap.Balances() for i := 0; i < TXs; i++ { send := gen.Int() % P @@ -278,6 +281,7 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d amt := basics.MicroAlgos{Raw: uint64(gen.Int() % xferMax)} fee := basics.MicroAlgos{Raw: uint64(gen.Int()%maxFee) + proto.MinTxnFee} + t := transactions.Transaction{ Type: protocol.PaymentTx, Header: transactions.Header{ @@ -310,5 +314,6 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d rbal.MicroAlgos.Raw += amt.Raw bal[raddr] = rbal } + return ledger, roots, parts, tx, release } diff --git a/data/genesisBalances.go b/data/genesisBalances.go index 20e408938d..e1b5040e1b 100644 --- a/data/genesisBalances.go +++ b/data/genesisBalances.go @@ -30,6 +30,11 @@ type GenesisBalances struct { timestamp int64 } +// Balances returns the balances field of GenesisBalances +func (gb GenesisBalances) Balances() map[basics.Address]basics.AccountData { + return gb.balances +} + // MakeGenesisBalances returns the information needed to bootstrap the ledger based on the current time func MakeGenesisBalances(balances map[basics.Address]basics.AccountData, feeSink, rewardsPool basics.Address) GenesisBalances { return MakeTimestampedGenesisBalances(balances, feeSink, rewardsPool, time.Now().Unix()) From 3599563785709aaf1debff7064917bcf145d4abd Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 9 Jun 2020 13:40:48 -0400 Subject: [PATCH 038/267] Fast Catchup - Enable goal commands and add unit tests 1. Enable the goal commands to use the fast catchup 2. Add unit test TestLargeAccountCountCatchpointGeneration 3. Add telemetry event for catchpoint file generation 4. Fix bug when creating a catchpoint for a period that has no account changes 5. Documentation for watchdogStreamReader 6. Add unit test TestBasicCatchpointWriter --- catchup/watchdogStreamReader.go | 25 ++++++-- cmd/goal/node.go | 58 ++++++++++++++++++ ledger/acctupdates.go | 52 ++++++++++++----- ledger/acctupdates_test.go | 89 ++++++++++++++++++++++++++++ ledger/catchpointwriter.go | 8 +++ ledger/catchpointwriter_test.go | 100 ++++++++++++++++++++++++++++++++ logging/telemetryspec/event.go | 23 ++++++++ 7 files changed, 334 insertions(+), 21 deletions(-) diff --git a/catchup/watchdogStreamReader.go b/catchup/watchdogStreamReader.go index 2023449cfe..825f8c6741 100644 --- a/catchup/watchdogStreamReader.go +++ b/catchup/watchdogStreamReader.go @@ -31,6 +31,12 @@ var ErrWatchdogStreamReaderTimerElapsed = fmt.Errorf("watchdog stream reader tim // ErrWatchdogStreamReaderReaderReachedDataLimit is returned when watchdogStreamReader was asked to read beyond the designated data limits var ErrWatchdogStreamReaderReaderReachedDataLimit = fmt.Errorf("watchdog stream reader reached data limit") +// watchdogStreamReader is a glorified io.Reader base structure, which adds two fail safes for the default io.Reader: +// 1. It allows to limit the amount of data being read from the stream between two subsequent Reset calls. +// 2. It allows to limit the amount of time being spent reading and/or waiting for data from the reader between two subsequent Reset calls. +// The intended usage is to attach it directly to a http.Response.Body and to perform the reading from the watchdogStreamReader directly. +// If data is being pulled as expected, the caller could extend the timeout and or data limits. This is particulary usefull when providing +// the input stream to components which wouldn't time-limit or data-limit the data, and could end up exhusting the host computer resources. type watchdogStreamReader struct { // watchdog configuration underlayingReader io.Reader // the underlaying data source @@ -42,14 +48,15 @@ type watchdogStreamReader struct { readError error // the outgoing reported error ( either coming from the underlaying data source or self-generated ) totalRead uint64 // the total amount of bytes read from the data source so far. - maxDataSize uint64 // the current high threshold for data reader - readerClose chan struct{} - tickerClose chan struct{} - readerRequest chan struct{} - readerMu deadlock.Mutex - readerCond *sync.Cond + maxDataSize uint64 // the current high threshold for data reader + readerClose chan struct{} // channel used to signal the shutting down of the reader goroutine + tickerClose chan struct{} // channel used to signal the shutting down of the ticker goroutine + readerRequest chan struct{} // channel used to signal the reader goroutine that it needs to go and attempt to read from the underlying reader + readerMu deadlock.Mutex // syncronization mutex for the reader goroutine + readerCond *sync.Cond // conditional check variable for the reader goroutine } +// makeWatchdogStreamReader creates a watchdogStreamReader and initializes it. func makeWatchdogStreamReader(underlayingReader io.Reader, readSize uint64, readaheadSize uint64, readaheadDuration time.Duration) *watchdogStreamReader { reader := &watchdogStreamReader{ underlayingReader: underlayingReader, @@ -68,6 +75,7 @@ func makeWatchdogStreamReader(underlayingReader io.Reader, readSize uint64, read return reader } +// Reset extends the time and data limits by another "block", as well as returns an error code if the data stream has reached to it's end. func (r *watchdogStreamReader) Reset() error { r.readerMu.Lock() if r.readError != nil && len(r.stageBuffer) == 0 { @@ -81,6 +89,7 @@ func (r *watchdogStreamReader) Reset() error { return nil } +// Read reads from the attached data stream, and aborts prematurally in case we've exceeed the data size or time allowed for the read to complete. func (r *watchdogStreamReader) Read(p []byte) (n int, err error) { r.readerMu.Lock() defer r.readerMu.Unlock() @@ -107,6 +116,8 @@ func (r *watchdogStreamReader) Read(p []byte) (n int, err error) { return } +// ticker is the internal watchdogStreamReader goroutine which tracks the timeout operations. It operates on a single deadline at a time, and aborts when it's done. +// the Reset would create a new ticker goroutine as needed. func (r *watchdogStreamReader) ticker() { timerCh := time.After(r.readaheadDuration) select { @@ -123,6 +134,7 @@ func (r *watchdogStreamReader) ticker() { } } +// puller is the internal watchdogStreamReader goroutine which pulls that data from the associated incoming data stream and stores it in the staging buffer. func (r *watchdogStreamReader) puller() { var n int for err := error(nil); err == nil; { @@ -149,6 +161,7 @@ func (r *watchdogStreamReader) puller() { } } +// Close shuts down the watchdogStreamReader and signals it's internal goroutines to be shut down. func (r *watchdogStreamReader) Close() { // signal the puller goroutine to shut down close(r.readerClose) diff --git a/cmd/goal/node.go b/cmd/goal/node.go index 69d6abafe1..0b5a139641 100644 --- a/cmd/goal/node.go +++ b/cmd/goal/node.go @@ -19,6 +19,7 @@ package main import ( "encoding/base64" "encoding/json" + "errors" "fmt" "net" "os" @@ -31,6 +32,7 @@ import ( generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/libgoal" "github.com/algorand/go-algorand/network" "github.com/algorand/go-algorand/nodecontrol" @@ -52,6 +54,7 @@ var newNodeArchival bool var newNodeIndexer bool var newNodeRelay string var watchMillisecond uint64 +var abortCatchup bool func init() { nodeCmd.AddCommand(startCmd) @@ -64,6 +67,7 @@ func init() { nodeCmd.AddCommand(pendingTxnsCmd) nodeCmd.AddCommand(waitCmd) nodeCmd.AddCommand(createCmd) + nodeCmd.AddCommand(catchupCmd) // Once the server-side implementation of the shutdown command is ready, we should enable this one. //nodeCmd.AddCommand(shutdownCmd) @@ -95,6 +99,8 @@ func init() { waitCmd.Flags().Uint32VarP(&waitSec, "waittime", "w", 5, "Time (in seconds) to wait for node to make progress") statusCmd.Flags().Uint64VarP(&watchMillisecond, "watch", "w", 0, "Time (in milliseconds) between two successive status updates") + catchupCmd.Flags().BoolVarP(&abortCatchup, "abort", "x", false, "Aborts the current catchup process") + } var nodeCmd = &cobra.Command{ @@ -108,6 +114,43 @@ var nodeCmd = &cobra.Command{ }, } +var catchupCmd = &cobra.Command{ + Use: "catchup", + Short: "Catchup the Algorand node to a specific catchpoint", + Long: "Catchup allows making large jumps over round ranges without the need to incremently validate each individual round.", + Example: "goal node catchup 6500000#1234567890ABCDEF01234567890ABCDEF0\tStart catching up to round 6500000 with the provided catchpoint\ngoal node catchup --abort\t\t\t\t\tAbort the current catchup", + Args: catchpointCmdArgument, + Run: func(cmd *cobra.Command, args []string) { + if abortCatchup == false && len(args) == 0 { + fmt.Println(errorCatchpointLabelMissing) + os.Exit(1) + } + onDataDirs(func(datadir string) { catchup(datadir, args) }) + }, +} + +func catchpointCmdArgument(cmd *cobra.Command, args []string) error { + catchpointsCount := 0 + for _, arg := range args { + _, _, err := ledger.ParseCatchpointLabel(arg) + switch err { + case nil: + if catchpointsCount > 0 { + return errors.New(errorTooManyCatchpointLabels) + } + catchpointsCount++ + continue + case ledger.ErrCatchpointParsingFailed: + // this isn't a valid catchpoint label. + // return a nice formatted error + return errors.New(errorCatchpointLabelParsingFailed) + default: + return err + } + } + return nil +} + var startCmd = &cobra.Command{ Use: "start", Short: "Initialize the specified Algorand node.", @@ -586,6 +629,21 @@ var createCmd = &cobra.Command{ }, } +func catchup(dataDir string, args []string) { + client := ensureAlgodClient(ensureSingleDataDir()) + if abortCatchup { + err := client.AbortCatchup() + if err != nil { + reportErrorf(errorNodeStatus, err) + } + return + } + err := client.Catchup(args[0]) + if err != nil { + reportErrorf(errorNodeStatus, err) + } +} + // verifyPeerDialArg verifies that the peers provided in peerDial are valid peers. func verifyPeerDialArg() bool { if peerDial == "" { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 6904e1697d..854d1b6a93 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -36,6 +36,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" ) @@ -227,7 +228,7 @@ func (au *accountUpdates) loadFromDisk(l ledgerForTracker) error { writingCatchpointDigest, err = au.initializeCaches(lastBalancesRound, lastestBlockRound, basics.Round(writingCatchpointRound)) if writingCatchpointRound != 0 && au.catchpointInterval != 0 { - au.generateCatchpoint(basics.Round(writingCatchpointRound), au.lastCatchpointLabel, writingCatchpointDigest) + au.generateCatchpoint(basics.Round(writingCatchpointRound), au.lastCatchpointLabel, writingCatchpointDigest, time.Duration(0)) } return nil @@ -873,17 +874,9 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Addre return } -func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Round, totals AccountTotals, ledgerBlockDigest crypto.Digest) (label string, err error) { - var balancesHash crypto.Digest - - balancesHash, err = au.balancesTrie.RootHash() - if err != nil { - return - } - - cpLabel := makeCatchpointLabel(committedRound, ledgerBlockDigest, balancesHash, totals) +func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Round, totals AccountTotals, ledgerBlockDigest crypto.Digest, trieBalancesHash crypto.Digest) (label string, err error) { + cpLabel := makeCatchpointLabel(committedRound, ledgerBlockDigest, trieBalancesHash, totals) label = cpLabel.String() - _, err = au.accountsq.writeCatchpointStateString(context.Background(), catchpointStateLastCatchpoint, label) return } @@ -997,7 +990,8 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } var catchpointLabel string - + beforeUpdatingBalancesTime := time.Now() + var trieBalancesHash crypto.Digest err := au.dbs.wdb.Atomic(func(tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) if au.catchpointInterval > 0 { @@ -1029,6 +1023,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) + if isCatchpointRound { + trieBalancesHash, err = au.balancesTrie.RootHash() + if err != nil { + return + } + } return err }) @@ -1038,7 +1038,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb return } if isCatchpointRound { - catchpointLabel, err = au.accountsCreateCatchpointLabel(dbRound+basics.Round(offset)+lookback, roundTotals[offset], committedRoundDigest) + catchpointLabel, err = au.accountsCreateCatchpointLabel(dbRound+basics.Round(offset)+lookback, roundTotals[offset], committedRoundDigest, trieBalancesHash) if err != nil { au.log.Warnf("commitRound : unable to create a catchpoint label: %v", err) } @@ -1054,6 +1054,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb if isCatchpointRound && catchpointLabel != "" { au.lastCatchpointLabel = catchpointLabel } + updatingBalancesDuration := time.Now().Sub(beforeUpdatingBalancesTime) // Drop reference counts to modified accounts, and evict them // from in-memory cache when no references remain. @@ -1101,11 +1102,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb au.assetDeltas = au.assetDeltas[offset:] au.dbRound = newBase au.lastFlushTime = flushTime + au.accountsMu.Unlock() if isCatchpointRound && au.archivalLedger && catchpointLabel != "" { // start generating the catchpoint on a separate goroutine. - au.generateCatchpoint(basics.Round(offset)+dbRound+lookback, catchpointLabel, committedRoundDigest) + au.generateCatchpoint(basics.Round(offset)+dbRound+lookback, catchpointLabel, committedRoundDigest, updatingBalancesDuration) } } @@ -1114,7 +1116,12 @@ func (au *accountUpdates) latest() basics.Round { return au.dbRound + basics.Round(len(au.deltas)) } -func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label string, committedRoundDigest crypto.Digest) { +func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label string, committedRoundDigest crypto.Digest, updatingBalancesDuration time.Duration) { + beforeGeneratingCatchpointTime := time.Now() + catchpointGenerationStats := telemetryspec.CatchpointGenerationEventDetails{ + BalancesWriteTime: uint64(updatingBalancesDuration.Nanoseconds()), + } + // the retryCatchpointCreation is used to repeat the catchpoint file generation in case the node crashed / aborted during startup // before the catchpoint file generation could be completed. retryCatchpointCreation := false @@ -1152,7 +1159,10 @@ func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label } for more { stepCtx, stepCancelFunction := context.WithTimeout(au.ctx, chunkExecutionDuration) + writeStepStartTime := time.Now() more, err = catchpointWriter.WriteStep(stepCtx) + // accumulate the actual time we've spent writing in this step. + catchpointGenerationStats.CPUTime += uint64(time.Now().Sub(writeStepStartTime).Nanoseconds()) stepCancelFunction() if more && err == nil { // we just wrote some data, but there is more to be written. @@ -1185,6 +1195,18 @@ func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label au.log.Warnf("accountUpdates: generateCatchpoint: unable to save catchpoint: %v", err) return } + catchpointGenerationStats.FileSize = uint64(catchpointWriter.GetSize()) + catchpointGenerationStats.WritingDuration = uint64(time.Now().Sub(beforeGeneratingCatchpointTime).Nanoseconds()) + catchpointGenerationStats.AccountsCount = catchpointWriter.GetTotalAccounts() + catchpointGenerationStats.CatchpointLabel = catchpointWriter.GetCatchpoint() + au.log.EventWithDetails(telemetryspec.Accounts, telemetryspec.CatchpointGenerationEvent, catchpointGenerationStats) + au.log.With("writingDuration", catchpointGenerationStats.WritingDuration). + With("CPUTime", catchpointGenerationStats.CPUTime). + With("balancesWriteTime", catchpointGenerationStats.BalancesWriteTime). + With("accountsCount", catchpointGenerationStats.AccountsCount). + With("fileSize", catchpointGenerationStats.FileSize). + With("catchpointLabel", catchpointGenerationStats.CatchpointLabel). + Infof("Catchpoint file was generated") } func catchpointRoundToPath(rnd basics.Round) string { diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 72a5e549e0..5a1f463109 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -19,6 +19,7 @@ package ledger import ( "database/sql" "fmt" + "os" "runtime" "sync" "testing" @@ -43,6 +44,7 @@ type mockLedgerForTracker struct { func makeMockLedgerForTracker(t testing.TB) *mockLedgerForTracker { dbs := dbOpenTest(t) dblogger := logging.TestingLog(t) + dblogger.SetLevel(logging.Info) dbs.rdb.SetLogger(dblogger) dbs.wdb.SetLogger(dblogger) return &mockLedgerForTracker{dbs: dbs, log: dblogger} @@ -507,3 +509,90 @@ func BenchmarkCalibrateCacheNodeSize(b *testing.B) { } trieCachedNodesCount = defaultTrieCachedNodesCount } + +// TestLargeAccountCountCatchpointGeneration creates a ledger containing a large set of accounts ( i.e. 100K accounts ) +// and attempts to have the accountUpdates create the associated catchpoint. It's designed precisly around setting an +// environment which would quickly ( i.e. after 32 rounds ) would start producing catchpoints. +func TestLargeAccountCountCatchpointGeneration(t *testing.T) { + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + t.Skip("This test is too slow on ARM and causes travis builds to time out") + } + // create new protocol version, which has lower back balance. + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestLargeAccountCountCatchpointGeneration") + protoParams := config.Consensus[protocol.ConsensusCurrentVersion] + protoParams.MaxBalLookback = 32 + protoParams.SeedLookback = 2 + protoParams.SeedRefreshInterval = 8 + config.Consensus[testProtocolVersion] = protoParams + defer func() { + delete(config.Consensus, testProtocolVersion) + os.RemoveAll("./catchpoints") + }() + + ml := makeMockLedgerForTracker(t) + defer ml.close() + ml.blocks = randomInitChain(testProtocolVersion, 10) + accts := []map[basics.Address]basics.AccountData{randomAccounts(100000)} + rewardsLevels := []uint64{0} + + pooldata := basics.AccountData{} + pooldata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + pooldata.Status = basics.NotParticipating + accts[0][testPoolAddr] = pooldata + + sinkdata := basics.AccountData{} + sinkdata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + sinkdata.Status = basics.NotParticipating + accts[0][testSinkAddr] = sinkdata + + au := &accountUpdates{} + conf := config.GetDefaultLocal() + conf.CatchpointInterval = 1 + conf.Archival = true + au.initialize(conf, ".", protoParams, accts[0]) + defer au.close() + err := au.loadFromDisk(ml) + require.NoError(t, err) + + // cover 10 genesis blocks + rewardLevel := uint64(0) + for i := 1; i < 10; i++ { + accts = append(accts, accts[0]) + rewardsLevels = append(rewardsLevels, rewardLevel) + } + + for i := basics.Round(10); i < basics.Round(protoParams.MaxBalLookback+5); i++ { + rewardLevelDelta := crypto.RandUint64() % 5 + rewardLevel += rewardLevelDelta + updates, totals := randomDeltasBalanced(1, accts[i-1], rewardLevel) + + prevTotals, err := au.totals(basics.Round(i - 1)) + require.NoError(t, err) + + oldPool := accts[i-1][testPoolAddr] + newPool := totals[testPoolAddr] + newPool.MicroAlgos.Raw -= prevTotals.RewardUnits() * rewardLevelDelta + updates[testPoolAddr] = accountDelta{old: oldPool, new: newPool} + totals[testPoolAddr] = newPool + + blk := bookkeeping.Block{ + BlockHeader: bookkeeping.BlockHeader{ + Round: basics.Round(i), + }, + } + blk.RewardsLevel = rewardLevel + blk.CurrentProtocol = testProtocolVersion + + au.newBlock(blk, StateDelta{ + accts: updates, + hdr: &blk.BlockHeader, + }) + accts = append(accts, totals) + rewardsLevels = append(rewardsLevels, rewardLevel) + + au.committedUpTo(i) + if i%2 == 1 { + au.waitAccountsWriting() + } + } +} diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index 539c263ee7..ff06fd4606 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -263,6 +263,14 @@ func (cw *catchpointWriter) GetBalancesRound() basics.Round { return basics.Round(0) } +// GetBalancesCount returns the number of balances written to this catchpoint file. +func (cw *catchpointWriter) GetTotalAccounts() uint64 { + if cw.fileHeader != nil { + return cw.fileHeader.TotalAccounts + } + return 0 +} + // GetCatchpoint returns the catchpoint string to which this catchpoint file was generated for. func (cw *catchpointWriter) GetCatchpoint() string { if cw.fileHeader != nil { diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index 89aa8c681e..de813e8b2c 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -17,6 +17,15 @@ package ledger import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" @@ -112,3 +121,94 @@ func TestCatchpointFileBalancesChunkEncoding(t *testing.T) { require.Equal(t, fbc, fbc2) } + +func TestBasicCatchpointWriter(t *testing.T) { + // create new protocol version, which has lower back balance. + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestBasicCatchpointWriter") + protoParams := config.Consensus[protocol.ConsensusCurrentVersion] + protoParams.MaxBalLookback = 32 + protoParams.SeedLookback = 2 + protoParams.SeedRefreshInterval = 8 + config.Consensus[testProtocolVersion] = protoParams + defer func() { + delete(config.Consensus, testProtocolVersion) + os.RemoveAll("./catchpoints") + }() + + ml := makeMockLedgerForTracker(t) + defer ml.close() + ml.blocks = randomInitChain(testProtocolVersion, 10) + accts := []map[basics.Address]basics.AccountData{randomAccounts(300)} + + au := &accountUpdates{} + conf := config.GetDefaultLocal() + conf.CatchpointInterval = 1 + conf.Archival = true + au.initialize(conf, ".", protoParams, accts[0]) + defer au.close() + err := au.loadFromDisk(ml) + require.NoError(t, err) + au.close() + fileName := filepath.Join("./catchpoints", "15.catchpoint") + blocksRound := basics.Round(12345) + blockHeaderDigest := crypto.Hash([]byte{1, 2, 3}) + catchpointLabel := fmt.Sprintf("%d#%v", blocksRound, blockHeaderDigest) // this is not a correct way to create a label, but it's good enough for this unit test + writer := makeCatchpointWriter(fileName, ml.trackerDB().rdb, blocksRound, blockHeaderDigest, catchpointLabel) + for { + more, err := writer.WriteStep(context.Background()) + require.NoError(t, err) + if !more { + break + } + } + + // load the file from disk. + fileContent, err := ioutil.ReadFile(fileName) + require.NoError(t, err) + gzipReader, err := gzip.NewReader(bytes.NewBuffer(fileContent)) + require.NoError(t, err) + tarReader := tar.NewReader(gzipReader) + defer gzipReader.Close() + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + require.NoError(t, err) + break + } + balancesBlockBytes := make([]byte, header.Size) + readComplete := int64(0) + + for readComplete < header.Size { + bytesRead, err := tarReader.Read(balancesBlockBytes[readComplete:]) + readComplete += int64(bytesRead) + if err != nil { + if err == io.EOF { + if readComplete == header.Size { + break + } + require.NoError(t, err) + } + break + } + } + if header.Name == "content.msgpack" { + var fileHeader catchpointFileHeader + err = protocol.Decode(balancesBlockBytes, &fileHeader) + require.NoError(t, err) + require.Equal(t, catchpointLabel, fileHeader.Catchpoint) + require.Equal(t, blocksRound, fileHeader.BlocksRound) + require.Equal(t, blockHeaderDigest, fileHeader.BlockHeaderDigest) + require.Equal(t, uint64(len(accts[0])), fileHeader.TotalAccounts) + } else if header.Name == "balances.1.1.msgpack" { + var balances catchpointFileBalancesChunk + err = protocol.Decode(balancesBlockBytes, &balances) + require.NoError(t, err) + require.Equal(t, uint64(len(accts[0])), uint64(len(balances.Balances))) + } else { + require.Failf(t, "unexpected tar chunk name %s", header.Name) + } + } +} diff --git a/logging/telemetryspec/event.go b/logging/telemetryspec/event.go index c528fcd9f5..ba1c79ea6c 100644 --- a/logging/telemetryspec/event.go +++ b/logging/telemetryspec/event.go @@ -282,3 +282,26 @@ type PeerConnectionDetails struct { // MessageDelay is the avarage relative message delay. Not being used for incoming connection. MessageDelay int64 `json:",omitempty"` } + +// CatchpointGenerationEvent event +const CatchpointGenerationEvent Event = "CatchpointGeneration" + +// CatchpointGenerationEventDetails is generated once a catchpoint file is being created, and provide +// some statistics about that event. +type CatchpointGenerationEventDetails struct { + // WritingDuration is the total elapsed time it took to write the catchpoint file. + WritingDuration uint64 + // CPUTime is the single-core time spent waiting to the catchpoint file to be written. + // this time excludes all the sleeping time taken, and represent the actual time it would + // take if we were doing the writing on a dedicated process + CPUTime uint64 + // BalancesWriteDuration is the time duration it took to write the balances portion + // ( i.e. update the account balances + update the trie ) + BalancesWriteTime uint64 + // AccountsCount is the number of accounts that were written into the generated catchpoint file + AccountsCount uint64 + // FileSize is the size of the catchpoint file, in bytes. + FileSize uint64 + // CatchpointLabel is the catchpoint label for which the catchpoint file was generated. + CatchpointLabel string +} From 13b7864f8268c4732797dfa55af4d7cd76df8fed Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 9 Jun 2020 16:20:13 -0400 Subject: [PATCH 039/267] FastCatchup - add unit testing for ledgerFetcher (#1147) Add testing for the ledgerFetcher --- catchup/catchpointService.go | 2 +- catchup/ledgerFetcher.go | 14 ++- catchup/ledgerFetcher_test.go | 98 ++++++++++++++++ .../mocks/mockCatchpointCatchupAccessor.go | 100 +++++++++++++++++ ledger/catchupaccessor.go | 105 +++++++++++++----- 5 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 catchup/ledgerFetcher_test.go create mode 100644 components/mocks/mockCatchpointCatchupAccessor.go diff --git a/catchup/catchpointService.go b/catchup/catchpointService.go index 4c8bbd3407..daa622c5f7 100644 --- a/catchup/catchpointService.go +++ b/catchup/catchpointService.go @@ -58,7 +58,7 @@ type CatchpointCatchupService struct { ctx context.Context cancelCtxFunc context.CancelFunc running sync.WaitGroup - ledgerAccessor *ledger.CatchpointCatchupAccessor + ledgerAccessor ledger.CatchpointCatchupAccessor stage ledger.CatchpointCatchupState log logging.Logger newService bool // indicates whether this service was created after the node was running ( i.e. true ) or the node just started to find that it was previously perfoming catchup diff --git a/catchup/ledgerFetcher.go b/catchup/ledgerFetcher.go index 92d516b850..1d6a8550a9 100644 --- a/catchup/ledgerFetcher.go +++ b/catchup/ledgerFetcher.go @@ -49,19 +49,22 @@ const ( catchpointFileStreamReadSize = 4096 ) +var errNoPeersAvailable = fmt.Errorf("downloadLedger : no peers are available") +var errNonHTTPPeer = fmt.Errorf("downloadLedger : non-HTTPPeer encountered") + type ledgerFetcherReporter interface { updateLedgerFetcherProgress(*ledger.CatchpointCatchupAccessorProgress) } type ledgerFetcher struct { net network.GossipNode - accessor *ledger.CatchpointCatchupAccessor + accessor ledger.CatchpointCatchupAccessor log logging.Logger peers []network.Peer reporter ledgerFetcherReporter } -func makeLedgerFetcher(net network.GossipNode, accessor *ledger.CatchpointCatchupAccessor, log logging.Logger, reporter ledgerFetcherReporter) *ledgerFetcher { +func makeLedgerFetcher(net network.GossipNode, accessor ledger.CatchpointCatchupAccessor, log logging.Logger, reporter ledgerFetcherReporter) *ledgerFetcher { return &ledgerFetcher{ net: net, accessor: accessor, @@ -74,13 +77,13 @@ func (lf *ledgerFetcher) downloadLedger(ctx context.Context, round basics.Round) if len(lf.peers) == 0 { lf.peers = lf.net.GetPeers(network.PeersPhonebook) if len(lf.peers) == 0 { - return fmt.Errorf("downloadLedger : no peers are available") + return errNoPeersAvailable } } peer, ok := lf.peers[0].(network.HTTPPeer) lf.peers = lf.peers[1:] if !ok { - return fmt.Errorf("downloadLedger : non-HTTPPeer encountered") + return errNonHTTPPeer } return lf.getPeerLedger(ctx, peer, round) } @@ -90,10 +93,11 @@ func (lf *ledgerFetcher) getPeerLedger(ctx context.Context, peer network.HTTPPee if err != nil { return err } + parsedURL.Path = peer.PrepareURL(path.Join(parsedURL.Path, "/v1/{genesisID}/ledger/"+strconv.FormatUint(uint64(round), 36))) ledgerURL := parsedURL.String() lf.log.Debugf("ledger GET %#v peer %#v %T", ledgerURL, peer, peer) - request, err := http.NewRequest("GET", ledgerURL, nil) + request, err := http.NewRequest(http.MethodGet, ledgerURL, nil) if err != nil { return err } diff --git a/catchup/ledgerFetcher_test.go b/catchup/ledgerFetcher_test.go new file mode 100644 index 0000000000..cccedc3f2b --- /dev/null +++ b/catchup/ledgerFetcher_test.go @@ -0,0 +1,98 @@ +// 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 . + +package catchup + +import ( + "context" + "fmt" + "net" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/components/mocks" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/logging" +) + +type dummyLedgerFetcherReporter struct { +} + +func (lf *dummyLedgerFetcherReporter) updateLedgerFetcherProgress(*ledger.CatchpointCatchupAccessorProgress) { +} + +func TestNoPeersAvailable(t *testing.T) { + lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}) + err := lf.downloadLedger(context.Background(), basics.Round(0)) + require.Equal(t, errNoPeersAvailable, err) + lf.peers = append(lf.peers, &lf) // The peer is an opaque interface.. we can add anything as a Peer. + err = lf.downloadLedger(context.Background(), basics.Round(0)) + require.Equal(t, errNonHTTPPeer, err) +} + +func TestNonParsableAddress(t *testing.T) { + lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}) + peer := testHTTPPeer(":def") + err := lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Error(t, err) +} + +func TestLedgerFetcherErrorResponseHandling(t *testing.T) { + // create a dummy server. + mux := http.NewServeMux() + s := &http.Server{ + Handler: mux, + } + listener, err := net.Listen("tcp", "localhost:") + + require.NoError(t, err) + go s.Serve(listener) + defer s.Close() + defer listener.Close() + + httpServerResponse := http.StatusNotFound + contentTypes := make([]string, 0) + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + for _, contentType := range contentTypes { + w.Header().Add("Content-Type", contentType) + } + w.WriteHeader(httpServerResponse) + }) + + lf := makeLedgerFetcher(&mocks.MockNetwork{}, &mocks.MockCatchpointCatchupAccessor{}, logging.TestingLog(t), &dummyLedgerFetcherReporter{}) + peer := testHTTPPeer(listener.Addr().String()) + err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Equal(t, errNoLedgerForRound, err) + + httpServerResponse = http.StatusInternalServerError + err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Equal(t, fmt.Errorf("getPeerLedger error response status code %d", httpServerResponse), err) + + httpServerResponse = http.StatusOK + err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Equal(t, fmt.Errorf("getPeerLedger : http ledger fetcher invalid content type count %d", 0), err) + + contentTypes = []string{"applications/one", "applications/two"} + err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Equal(t, fmt.Errorf("getPeerLedger : http ledger fetcher invalid content type count %d", len(contentTypes)), err) + + contentTypes = []string{"applications/one"} + err = lf.getPeerLedger(context.Background(), &peer, basics.Round(0)) + require.Equal(t, fmt.Errorf("getPeerLedger : http ledger fetcher response has an invalid content type : %s", contentTypes[0]), err) +} diff --git a/components/mocks/mockCatchpointCatchupAccessor.go b/components/mocks/mockCatchpointCatchupAccessor.go new file mode 100644 index 0000000000..53363e7bc8 --- /dev/null +++ b/components/mocks/mockCatchpointCatchupAccessor.go @@ -0,0 +1,100 @@ +// 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 . + +package mocks + +import ( + "context" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/ledger" +) + +// MockCatchpointCatchupAccessor is a dummy CatchpointCatchupAccessor implementation which doesn't do anything. +type MockCatchpointCatchupAccessor struct{} + +// GetState returns the current state of the catchpoint catchup +func (m *MockCatchpointCatchupAccessor) GetState(ctx context.Context) (state ledger.CatchpointCatchupState, err error) { + return ledger.CatchpointCatchupStateInactive, nil +} + +// SetState set the state of the catchpoint catchup +func (m *MockCatchpointCatchupAccessor) SetState(ctx context.Context, state ledger.CatchpointCatchupState) (err error) { + return nil +} + +// GetLabel returns the current catchpoint catchup label +func (m *MockCatchpointCatchupAccessor) GetLabel(ctx context.Context) (label string, err error) { + return "", nil +} + +// SetLabel set the catchpoint catchup label +func (m *MockCatchpointCatchupAccessor) SetLabel(ctx context.Context, label string) (err error) { + return nil +} + +// ResetStagingBalances resets the current staging balances, preparing for a new set of balances to be added +func (m *MockCatchpointCatchupAccessor) ResetStagingBalances(ctx context.Context, newCatchup bool) (err error) { + return nil +} + +// ProgressStagingBalances deserialize the given bytes as a temporary staging balances +func (m *MockCatchpointCatchupAccessor) ProgressStagingBalances(ctx context.Context, sectionName string, bytes []byte, progress *ledger.CatchpointCatchupAccessorProgress) (err error) { + return nil +} + +// GetCatchupBlockRound returns the latest block round matching the current catchpoint +func (m *MockCatchpointCatchupAccessor) GetCatchupBlockRound(ctx context.Context) (round basics.Round, err error) { + return basics.Round(0), nil +} + +// VerifyCatchpoint verifies that the catchpoint is valid by reconstructing the label. +func (m *MockCatchpointCatchupAccessor) VerifyCatchpoint(ctx context.Context, blk *bookkeeping.Block) (err error) { + return nil +} + +// StoreBalancesRound calculates the balances round based on the first block and the associated consensus parametets, and +// store that to the database +func (m *MockCatchpointCatchupAccessor) StoreBalancesRound(ctx context.Context, blk *bookkeeping.Block) (err error) { + return nil +} + +// StoreFirstBlock stores a single block to the blocks database. +func (m *MockCatchpointCatchupAccessor) StoreFirstBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { + return nil +} + +// StoreBlock stores a single block to the blocks database. +func (m *MockCatchpointCatchupAccessor) StoreBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { + return nil +} + +// FinishBlocks concludes the catchup of the blocks database. +func (m *MockCatchpointCatchupAccessor) FinishBlocks(ctx context.Context, applyChanges bool) (err error) { + return nil +} + +// EnsureFirstBlock ensure that we have a single block in the staging block table, and returns that block +func (m *MockCatchpointCatchupAccessor) EnsureFirstBlock(ctx context.Context) (blk bookkeeping.Block, err error) { + return bookkeeping.Block{}, nil +} + +// CompleteCatchup completes the catchpoint catchup process by switching the databases tables around +// and reloading the ledger. +func (m *MockCatchpointCatchupAccessor) CompleteCatchup(ctx context.Context) (err error) { + return nil +} diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index 4176b41cc6..68612d9163 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -31,8 +31,55 @@ import ( "github.com/algorand/go-algorand/protocol" ) -// CatchpointCatchupAccessor is an accessor wrapping the database storage for the catchpoint catchup functionality. -type CatchpointCatchupAccessor struct { +// CatchpointCatchupAccessor is an interface for the accessor wrapping the database storage for the catchpoint catchup functionality. +type CatchpointCatchupAccessor interface { + // GetState returns the current state of the catchpoint catchup + GetState(ctx context.Context) (state CatchpointCatchupState, err error) + + // SetState set the state of the catchpoint catchup + SetState(ctx context.Context, state CatchpointCatchupState) (err error) + + // GetLabel returns the current catchpoint catchup label + GetLabel(ctx context.Context) (label string, err error) + + // SetLabel set the catchpoint catchup label + SetLabel(ctx context.Context, label string) (err error) + + // ResetStagingBalances resets the current staging balances, preparing for a new set of balances to be added + ResetStagingBalances(ctx context.Context, newCatchup bool) (err error) + + // ProgressStagingBalances deserialize the given bytes as a temporary staging balances + ProgressStagingBalances(ctx context.Context, sectionName string, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) + + // GetCatchupBlockRound returns the latest block round matching the current catchpoint + GetCatchupBlockRound(ctx context.Context) (round basics.Round, err error) + + // VerifyCatchpoint verifies that the catchpoint is valid by reconstructing the label. + VerifyCatchpoint(ctx context.Context, blk *bookkeeping.Block) (err error) + + // StoreBalancesRound calculates the balances round based on the first block and the associated consensus parametets, and + // store that to the database + StoreBalancesRound(ctx context.Context, blk *bookkeeping.Block) (err error) + + // StoreFirstBlock stores a single block to the blocks database. + StoreFirstBlock(ctx context.Context, blk *bookkeeping.Block) (err error) + + // StoreBlock stores a single block to the blocks database. + StoreBlock(ctx context.Context, blk *bookkeeping.Block) (err error) + + // FinishBlocks concludes the catchup of the blocks database. + FinishBlocks(ctx context.Context, applyChanges bool) (err error) + + // EnsureFirstBlock ensure that we have a single block in the staging block table, and returns that block + EnsureFirstBlock(ctx context.Context) (blk bookkeeping.Block, err error) + + // CompleteCatchup completes the catchpoint catchup process by switching the databases tables around + // and reloading the ledger. + CompleteCatchup(ctx context.Context) (err error) +} + +// CatchpointCatchupAccessorImpl is the concrete implementation of the CatchpointCatchupAccessor interface +type CatchpointCatchupAccessorImpl struct { ledger *Ledger // log copied from ledger @@ -62,7 +109,7 @@ const ( ) // MakeCatchpointCatchupAccessor creates a CatchpointCatchupAccessor given a ledger -func MakeCatchpointCatchupAccessor(ledger *Ledger, log logging.Logger) *CatchpointCatchupAccessor { +func MakeCatchpointCatchupAccessor(ledger *Ledger, log logging.Logger) CatchpointCatchupAccessor { rdb := ledger.trackerDB().rdb wdb := ledger.trackerDB().wdb accountsq, err := accountsDbInit(rdb.Handle, wdb.Handle) @@ -70,7 +117,7 @@ func MakeCatchpointCatchupAccessor(ledger *Ledger, log logging.Logger) *Catchpoi log.Warnf("unable to initialize account db in MakeCatchpointCatchupAccessor : %v", err) return nil } - return &CatchpointCatchupAccessor{ + return &CatchpointCatchupAccessorImpl{ ledger: ledger, log: log, accountsq: accountsq, @@ -78,7 +125,7 @@ func MakeCatchpointCatchupAccessor(ledger *Ledger, log logging.Logger) *Catchpoi } // GetState returns the current state of the catchpoint catchup -func (c *CatchpointCatchupAccessor) GetState(ctx context.Context) (state CatchpointCatchupState, err error) { +func (c *CatchpointCatchupAccessorImpl) GetState(ctx context.Context) (state CatchpointCatchupState, err error) { var istate uint64 istate, _, err = c.accountsq.readCatchpointStateUint64(ctx, catchpointStateCatchupState) if err != nil { @@ -89,7 +136,7 @@ func (c *CatchpointCatchupAccessor) GetState(ctx context.Context) (state Catchpo } // SetState set the state of the catchpoint catchup -func (c *CatchpointCatchupAccessor) SetState(ctx context.Context, state CatchpointCatchupState) (err error) { +func (c *CatchpointCatchupAccessorImpl) SetState(ctx context.Context, state CatchpointCatchupState) (err error) { if state < CatchpointCatchupStateInactive || state > catchpointCatchupStateLast { return fmt.Errorf("invalid catchpoint catchup state provided : %d", state) } @@ -101,7 +148,7 @@ func (c *CatchpointCatchupAccessor) SetState(ctx context.Context, state Catchpoi } // GetLabel returns the current catchpoint catchup label -func (c *CatchpointCatchupAccessor) GetLabel(ctx context.Context) (label string, err error) { +func (c *CatchpointCatchupAccessorImpl) GetLabel(ctx context.Context) (label string, err error) { label, _, err = c.accountsq.readCatchpointStateString(ctx, catchpointStateCatchupLabel) if err != nil { return "", fmt.Errorf("unable to read catchpoint catchup state '%s': %v", catchpointStateCatchupLabel, err) @@ -110,7 +157,7 @@ func (c *CatchpointCatchupAccessor) GetLabel(ctx context.Context) (label string, } // SetLabel set the catchpoint catchup label -func (c *CatchpointCatchupAccessor) SetLabel(ctx context.Context, label string) (err error) { +func (c *CatchpointCatchupAccessorImpl) SetLabel(ctx context.Context, label string) (err error) { // verify it's parsable : _, _, err = ParseCatchpointLabel(label) if err != nil { @@ -124,7 +171,7 @@ func (c *CatchpointCatchupAccessor) SetLabel(ctx context.Context, label string) } // ResetStagingBalances resets the current staging balances, preparing for a new set of balances to be added -func (c *CatchpointCatchupAccessor) ResetStagingBalances(ctx context.Context, newCatchup bool) (err error) { +func (c *CatchpointCatchupAccessorImpl) ResetStagingBalances(ctx context.Context, newCatchup bool) (err error) { wdb := c.ledger.trackerDB().wdb err = wdb.Atomic(func(tx *sql.Tx) (err error) { err = resetCatchpointStagingBalances(ctx, tx, newCatchup) @@ -170,7 +217,7 @@ type CatchpointCatchupAccessorProgress struct { } // ProgressStagingBalances deserialize the given bytes as a temporary staging balances -func (c *CatchpointCatchupAccessor) ProgressStagingBalances(ctx context.Context, sectionName string, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { +func (c *CatchpointCatchupAccessorImpl) ProgressStagingBalances(ctx context.Context, sectionName string, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { if sectionName == "content.msgpack" { return c.processStagingContent(ctx, bytes, progress) } @@ -178,14 +225,14 @@ func (c *CatchpointCatchupAccessor) ProgressStagingBalances(ctx context.Context, return c.processStagingBalances(ctx, bytes, progress) } // we want to allow undefined sections to support backward compatibility. - c.log.Warnf("CatchpointCatchupAccessor::ProgressStagingBalances encountered unexpected section name '%s' of length %d, which would be ignored", sectionName, len(bytes)) + c.log.Warnf("CatchpointCatchupAccessorImpl::ProgressStagingBalances encountered unexpected section name '%s' of length %d, which would be ignored", sectionName, len(bytes)) return nil } // processStagingContent deserialize the given bytes as a temporary staging balances content -func (c *CatchpointCatchupAccessor) processStagingContent(ctx context.Context, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { +func (c *CatchpointCatchupAccessorImpl) processStagingContent(ctx context.Context, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { if progress.SeenHeader { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingContent: content chunk already seen") + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: content chunk already seen") } var fileHeader catchpointFileHeader err = protocol.Decode(bytes, &fileHeader) @@ -193,7 +240,7 @@ func (c *CatchpointCatchupAccessor) processStagingContent(ctx context.Context, b return err } if fileHeader.Version != catchpointFileVersion { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingContent: unable to process catchpoint - version %d is not supported", fileHeader.Version) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to process catchpoint - version %d is not supported", fileHeader.Version) } // the following fields are now going to be ignored. We could add these to the database and validate these @@ -203,11 +250,11 @@ func (c *CatchpointCatchupAccessor) processStagingContent(ctx context.Context, b err = wdb.Atomic(func(tx *sql.Tx) (err error) { sq, err := accountsDbInit(tx, tx) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingContent: unable to initialize accountsDbInit: %v", err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to initialize accountsDbInit: %v", err) } _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBlockRound, uint64(fileHeader.BlocksRound)) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingContent: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) } err = accountsPutTotals(tx, fileHeader.Totals, true) return @@ -221,9 +268,9 @@ func (c *CatchpointCatchupAccessor) processStagingContent(ctx context.Context, b } // processStagingBalances deserialize the given bytes as a temporary staging balances -func (c *CatchpointCatchupAccessor) processStagingBalances(ctx context.Context, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { +func (c *CatchpointCatchupAccessorImpl) processStagingBalances(ctx context.Context, bytes []byte, progress *CatchpointCatchupAccessorProgress) (err error) { if !progress.SeenHeader { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingBalances: content chunk was missing") + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingBalances: content chunk was missing") } var balances catchpointFileBalancesChunk @@ -273,7 +320,7 @@ func (c *CatchpointCatchupAccessor) processStagingBalances(ctx context.Context, hash := accountHashBuilder(balance.Address, accountData, balance.AccountData) added, err := trie.Add(hash) if !added { - return fmt.Errorf("CatchpointCatchupAccessor::processStagingBalances: The provided catchpoint file contained the same account more than once. Account address %#v, account data %#v", balance.Address, accountData) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingBalances: The provided catchpoint file contained the same account more than once. Account address %#v, account data %#v", balance.Address, accountData) } if err != nil { return err @@ -293,7 +340,7 @@ func (c *CatchpointCatchupAccessor) processStagingBalances(ctx context.Context, } // GetCatchupBlockRound returns the latest block round matching the current catchpoint -func (c *CatchpointCatchupAccessor) GetCatchupBlockRound(ctx context.Context) (round basics.Round, err error) { +func (c *CatchpointCatchupAccessorImpl) GetCatchupBlockRound(ctx context.Context) (round basics.Round, err error) { var iRound uint64 iRound, _, err = c.accountsq.readCatchpointStateUint64(ctx, catchpointStateCatchupBlockRound) if err != nil { @@ -303,7 +350,7 @@ func (c *CatchpointCatchupAccessor) GetCatchupBlockRound(ctx context.Context) (r } // VerifyCatchpoint verifies that the catchpoint is valid by reconstructing the label. -func (c *CatchpointCatchupAccessor) VerifyCatchpoint(ctx context.Context, blk *bookkeeping.Block) (err error) { +func (c *CatchpointCatchupAccessorImpl) VerifyCatchpoint(ctx context.Context, blk *bookkeeping.Block) (err error) { rdb := c.ledger.trackerDB().rdb var balancesHash crypto.Digest var blockRound basics.Round @@ -362,7 +409,7 @@ func (c *CatchpointCatchupAccessor) VerifyCatchpoint(ctx context.Context, blk *b // StoreBalancesRound calculates the balances round based on the first block and the associated consensus parametets, and // store that to the database -func (c *CatchpointCatchupAccessor) StoreBalancesRound(ctx context.Context, blk *bookkeeping.Block) (err error) { +func (c *CatchpointCatchupAccessorImpl) StoreBalancesRound(ctx context.Context, blk *bookkeeping.Block) (err error) { // calculate the balances round and store it. It *should* be identical to the one in the catchpoint file header, but we don't want to // trust the one in the catchpoint file header, so we'll calculate it ourselves. balancesRound := blk.Round() - basics.Round(config.Consensus[blk.CurrentProtocol].MaxBalLookback) @@ -371,7 +418,7 @@ func (c *CatchpointCatchupAccessor) StoreBalancesRound(ctx context.Context, blk sq, err := accountsDbInit(tx, tx) _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBalancesRound, uint64(balancesRound)) if err != nil { - return fmt.Errorf("CatchpointCatchupAccessor::StoreBalancesRound: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBalancesRound, err) + return fmt.Errorf("CatchpointCatchupAccessorImpl::StoreBalancesRound: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBalancesRound, err) } return }) @@ -379,7 +426,7 @@ func (c *CatchpointCatchupAccessor) StoreBalancesRound(ctx context.Context, blk } // StoreFirstBlock stores a single block to the blocks database. -func (c *CatchpointCatchupAccessor) StoreFirstBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { +func (c *CatchpointCatchupAccessorImpl) StoreFirstBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { blockDbs := c.ledger.blockDB() err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { return blockStartCatchupStaging(tx, *blk) @@ -391,7 +438,7 @@ func (c *CatchpointCatchupAccessor) StoreFirstBlock(ctx context.Context, blk *bo } // StoreBlock stores a single block to the blocks database. -func (c *CatchpointCatchupAccessor) StoreBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { +func (c *CatchpointCatchupAccessorImpl) StoreBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { blockDbs := c.ledger.blockDB() err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { return blockPutStaging(tx, *blk) @@ -403,7 +450,7 @@ func (c *CatchpointCatchupAccessor) StoreBlock(ctx context.Context, blk *bookkee } // FinishBlocks concludes the catchup of the blocks database. -func (c *CatchpointCatchupAccessor) FinishBlocks(ctx context.Context, applyChanges bool) (err error) { +func (c *CatchpointCatchupAccessorImpl) FinishBlocks(ctx context.Context, applyChanges bool) (err error) { blockDbs := c.ledger.blockDB() err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { if applyChanges { @@ -418,7 +465,7 @@ func (c *CatchpointCatchupAccessor) FinishBlocks(ctx context.Context, applyChang } // EnsureFirstBlock ensure that we have a single block in the staging block table, and returns that block -func (c *CatchpointCatchupAccessor) EnsureFirstBlock(ctx context.Context) (blk bookkeeping.Block, err error) { +func (c *CatchpointCatchupAccessorImpl) EnsureFirstBlock(ctx context.Context) (blk bookkeeping.Block, err error) { blockDbs := c.ledger.blockDB() err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { blk, err = blockEnsureSingleBlock(tx) @@ -432,7 +479,7 @@ func (c *CatchpointCatchupAccessor) EnsureFirstBlock(ctx context.Context) (blk b // CompleteCatchup completes the catchpoint catchup process by switching the databases tables around // and reloading the ledger. -func (c *CatchpointCatchupAccessor) CompleteCatchup(ctx context.Context) (err error) { +func (c *CatchpointCatchupAccessorImpl) CompleteCatchup(ctx context.Context) (err error) { err = c.FinishBlocks(ctx, true) if err != nil { return err @@ -446,7 +493,7 @@ func (c *CatchpointCatchupAccessor) CompleteCatchup(ctx context.Context) (err er } // finishBalances concludes the catchup of the balances(tracker) database. -func (c *CatchpointCatchupAccessor) finishBalances(ctx context.Context) (err error) { +func (c *CatchpointCatchupAccessorImpl) finishBalances(ctx context.Context) (err error) { wdb := c.ledger.trackerDB().wdb err = wdb.Atomic(func(tx *sql.Tx) (err error) { var balancesRound uint64 From dcf6ed2270f226a4301f998eac0a8ec990502616 Mon Sep 17 00:00:00 2001 From: John Lee Date: Wed, 10 Jun 2020 11:49:33 -0400 Subject: [PATCH 040/267] Match msgp version in configure dev deps and go.mod (#1150) The configure_dev-deps.sh script installs latest msgp, which conflicts with the later request to install a specific msgp version in go.mod. This is a quick fix to grep the version from go.mod and use it in the case of msgp. This appears to be the only module referenced in configure_dev-deps.sh and go.mod. --- scripts/configure_dev-deps.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/configure_dev-deps.sh b/scripts/configure_dev-deps.sh index 6aa1caf853..52650e5267 100755 --- a/scripts/configure_dev-deps.sh +++ b/scripts/configure_dev-deps.sh @@ -2,9 +2,24 @@ set -ex +function get_go_version { + ( + cd $(dirname "$0") + VERSION=$(cat ../go.mod | grep "$1" 2>/dev/null | awk -F " " '{print $2}') + echo $VERSION + ) + return +} + function install_go_module { local OUTPUT - OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) + # Check for version to go.mod version + VERSION=$(get_go_version "$1") + if [ -z "$VERSION" ]; then + OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) + else + OUTPUT=$(cd && GO111MODULE=on go get "$1@${VERSION}" 2>&1) + fi if [ "${OUTPUT}" != "" ]; then echo "error: executing \"go get -u $1\" failed : ${OUTPUT}" exit 1 From c538febbf912c26b62bf5b1d8cfe4cfc9a7839c5 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Wed, 10 Jun 2020 13:15:12 -0400 Subject: [PATCH 041/267] remove Balances(), it's unneeded --- daemon/algod/api/server/v2/test/helpers.go | 2 +- data/genesisBalances.go | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 7566a36fe8..3d017acaff 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -257,7 +257,7 @@ func testingenv(t testing.TB, numAccounts, numTxs int, offlineAccounts bool) (*d if latest != 0 { panic(fmt.Errorf("newly created ledger doesn't start on round 0")) } - bal := bootstrap.Balances() + bal := genesis // the current balance record is the same as the genesis balance record for i := 0; i < TXs; i++ { send := gen.Int() % P diff --git a/data/genesisBalances.go b/data/genesisBalances.go index e1b5040e1b..20e408938d 100644 --- a/data/genesisBalances.go +++ b/data/genesisBalances.go @@ -30,11 +30,6 @@ type GenesisBalances struct { timestamp int64 } -// Balances returns the balances field of GenesisBalances -func (gb GenesisBalances) Balances() map[basics.Address]basics.AccountData { - return gb.balances -} - // MakeGenesisBalances returns the information needed to bootstrap the ledger based on the current time func MakeGenesisBalances(balances map[basics.Address]basics.AccountData, feeSink, rewardsPool basics.Address) GenesisBalances { return MakeTimestampedGenesisBalances(balances, feeSink, rewardsPool, time.Now().Unix()) From 0312bfb30d20dbd94b5adfcda03e1a7948893e81 Mon Sep 17 00:00:00 2001 From: John Lee Date: Wed, 10 Jun 2020 17:16:37 -0400 Subject: [PATCH 042/267] Revert "Match msgp version in configure dev deps and go.mod (#1150)" (#1155) This reverts commit dcf6ed2270f226a4301f998eac0a8ec990502616. --- scripts/configure_dev-deps.sh | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/scripts/configure_dev-deps.sh b/scripts/configure_dev-deps.sh index 52650e5267..6aa1caf853 100755 --- a/scripts/configure_dev-deps.sh +++ b/scripts/configure_dev-deps.sh @@ -2,24 +2,9 @@ set -ex -function get_go_version { - ( - cd $(dirname "$0") - VERSION=$(cat ../go.mod | grep "$1" 2>/dev/null | awk -F " " '{print $2}') - echo $VERSION - ) - return -} - function install_go_module { local OUTPUT - # Check for version to go.mod version - VERSION=$(get_go_version "$1") - if [ -z "$VERSION" ]; then - OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) - else - OUTPUT=$(cd && GO111MODULE=on go get "$1@${VERSION}" 2>&1) - fi + OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) if [ "${OUTPUT}" != "" ]; then echo "error: executing \"go get -u $1\" failed : ${OUTPUT}" exit 1 From ee30a538663620f68429a01cc1ab08f7441208f3 Mon Sep 17 00:00:00 2001 From: Evan Richard Date: Thu, 11 Jun 2020 09:45:21 -0400 Subject: [PATCH 043/267] minor style change (mainly to kick Travis) --- daemon/algod/api/server/v2/test/handlers_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 22b7db92cc..51f6f716a3 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -76,7 +76,7 @@ func accountInformationTest(t *testing.T, address string, expectedCode int) { func TestAccountInformation(t *testing.T) { accountInformationTest(t, poolAddr.String(), 200) - accountInformationTest(t, "malformed", 400) + accountInformationTest(t, "bad account", 400) } func getBlockTest(t *testing.T, blockNum uint64, format string, expectedCode int) { @@ -91,7 +91,7 @@ func TestGetBlock(t *testing.T) { getBlockTest(t, 0, "json", 200) getBlockTest(t, 0, "msgpack", 200) getBlockTest(t, 1, "json", 500) - getBlockTest(t, 0, "malformed", 400) + getBlockTest(t, 0, "bad format", 400) } func TestGetSupply(t *testing.T) { @@ -150,7 +150,7 @@ func TestGetTransactionParams(t *testing.T) { func pendingTransactionInformationTest(t *testing.T, txidToUse int, format string, expectedCode int) { handler, c, rec, _, stxns, releasefunc := setupTestForMethodGet(t) defer releasefunc() - txid := "badtxid" + txid := "bad txid" if txidToUse >= 0 { txid = stxns[txidToUse].ID().String() } @@ -260,7 +260,7 @@ func startCatchupTest(t *testing.T, catchpoint string, expectedCode int) { func TestStartCatchup(t *testing.T) { goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" startCatchupTest(t, goodCatchPoint, 200) - badCatchPoint := "this is an invalid catchpoint" + badCatchPoint := "bad catchpoint" startCatchupTest(t, badCatchPoint, 400) } @@ -289,7 +289,7 @@ func abortCatchupTest(t *testing.T, catchpoint string, expectedCode int) { func TestAbortCatchup(t *testing.T) { goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" abortCatchupTest(t, goodCatchPoint, 200) - badCatchPoint := "this is an invalid catchpoint" + badCatchPoint := "bad catchpoint" abortCatchupTest(t, badCatchPoint, 400) } @@ -320,7 +320,7 @@ func TestTealCompile(t *testing.T) { goodProgram := `int 1` goodProgramBytes := []byte(goodProgram) tealCompileTest(t, goodProgramBytes, 200) - badProgram := "this is incorrect TEAL" + badProgram := "bad program" badProgramBytes := []byte(badProgram) tealCompileTest(t, badProgramBytes, 400) } From efb94bb1072e3a242ef3137e8e6da1b097a0cde8 Mon Sep 17 00:00:00 2001 From: egieseke Date: Thu, 11 Jun 2020 13:20:23 -0400 Subject: [PATCH 044/267] Eric/fix pingpong asset (#1129) Updated ping-pong utility to improve support for asset testing. Modified setup to use new accounts and each has its own set of assets, which are opted in and distributed to the other participant accounts. --- shared/pingpong/accounts.go | 326 +++++++++++++++++++++--------------- shared/pingpong/pingpong.go | 155 +++++++++++++---- 2 files changed, 315 insertions(+), 166 deletions(-) diff --git a/shared/pingpong/accounts.go b/shared/pingpong/accounts.go index 53104bba89..f2063c5143 100644 --- a/shared/pingpong/accounts.go +++ b/shared/pingpong/accounts.go @@ -19,8 +19,11 @@ package pingpong import ( "fmt" "github.com/algorand/go-algorand/crypto" - v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/libgoal" + "math" + "os" "sort" "time" ) @@ -82,186 +85,233 @@ func ensureAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]ui fmt.Printf("Located Source Account: %s -> %v\n", cfg.SrcAccount, accounts[cfg.SrcAccount]) } - // If we have more accounts than requested, pick the top N (not including src) - if len(accounts) > int(cfg.NumPartAccounts+1) { - fmt.Printf("Finding the richest %d accounts to use for transacting\n", cfg.NumPartAccounts) - accounts = takeTopAccounts(accounts, cfg.NumPartAccounts, cfg.SrcAccount) - } else { - // Not enough accounts yet (or just enough). Create more if needed - if len(accounts) != int(cfg.NumPartAccounts+1) { - fmt.Printf("Not enough accounts - creating %d more\n", int(cfg.NumPartAccounts+1)-len(accounts)) - } - accounts, err = generateAccounts(ac, accounts, cfg.NumPartAccounts, wallet) - if err != nil { - return + // Only reuse existing accounts for non asset testing. + // For asset testing, new participant accounts will be created since accounts are limited to 1000 assets. + if cfg.NumAsset == 0 { + + // If we have more accounts than requested, pick the top N (not including src) + if len(accounts) > int(cfg.NumPartAccounts+1) { + fmt.Printf("Finding the richest %d accounts to use for transacting\n", cfg.NumPartAccounts) + accounts = takeTopAccounts(accounts, cfg.NumPartAccounts, cfg.SrcAccount) + } else { + // Not enough accounts yet (or just enough). Create more if needed + if len(accounts) != int(cfg.NumPartAccounts+1) { + fmt.Printf("Not enough accounts - creating %d more\n", int(cfg.NumPartAccounts+1)-len(accounts)) + } + accounts, err = generateAccounts(ac, accounts, cfg.NumPartAccounts, wallet) + if err != nil { + return + } } } return } -func prepareAssets(accounts map[string]uint64, client libgoal.Client, cfg PpConfig) (assetParams map[uint64]v1.AssetParams, err error) { - // get existing assets - account, accountErr := client.AccountInformation(cfg.SrcAccount) - if accountErr != nil { - fmt.Printf("Cannot lookup source account") - err = accountErr - return +// throttle transaction rate +func throttleTransactionRate(startTime time.Time, cfg PpConfig, totalSent uint64) { + localTimeDelta := time.Now().Sub(startTime) + currentTps := float64(totalSent) / localTimeDelta.Seconds() + if currentTps > float64(cfg.TxnPerSec) { + sleepSec := float64(totalSent)/float64(cfg.TxnPerSec) - localTimeDelta.Seconds() + sleepTime := time.Duration(int64(math.Round(sleepSec*1000))) * time.Millisecond + time.Sleep(sleepTime) } +} - // Get wallet handle token - var h []byte - h, err = client.GetUnencryptedWalletHandle() - if err != nil { - return - } +// Prepare assets for asset transaction testing +// Step 1) Create X assets for each of the participant accounts +// Step 2) For each participant account, opt-in to assets of all other participant accounts +// Step 3) Evenly distribute the assets across all participant accounts +func prepareAssets(accounts map[string]uint64, client libgoal.Client, cfg PpConfig) (resultAssetMaps map[uint64]v1.AssetParams, err error) { - toCreate := int(cfg.NumAsset) - len(account.AssetParams) + var startTime = time.Now() + var totalSent uint64 = 0 + resultAssetMaps = make(map[uint64]v1.AssetParams) - // create assets in srcAccount - for i := 0; i < toCreate; i++ { - var metaLen = 32 - meta := make([]byte, metaLen, metaLen) - crypto.RandBytes(meta[:]) - totalSupply := cfg.MinAccountAsset * uint64(cfg.NumPartAccounts) * 9 - if totalSupply < cfg.MinAccountAsset { //overflow - fmt.Printf("Too many NumPartAccounts") - return - } - tx, createErr := client.MakeUnsignedAssetCreateTx(totalSupply, false, cfg.SrcAccount, cfg.SrcAccount, cfg.SrcAccount, cfg.SrcAccount, "ping", "pong", "", meta, 0) - if createErr != nil { - fmt.Printf("Cannot make asset create txn\n") - err = createErr - return - } - tx, err = client.FillUnsignedTxTemplate(cfg.SrcAccount, 0, 0, cfg.MaxFee, tx) - if err != nil { - fmt.Printf("Cannot fill asset creation txn\n") + // 1) Create X assets for each of the participant accounts + for addr := range accounts { + addrAccount, addrErr := client.AccountInformation(addr) + if addrErr != nil { + fmt.Printf("Cannot lookup source account %v\n", addr) + err = addrErr return } - signedTxn, signErr := client.SignTransactionWithWallet(h, nil, tx) - if signErr != nil { - fmt.Printf("Cannot sign asset creation txn\n") - err = signErr - return - } + toCreate := int(cfg.NumAsset) - len(addrAccount.AssetParams) - txid, broadcastErr := client.BroadcastTransaction(signedTxn) - if broadcastErr != nil { - fmt.Printf("Cannot broadcast asset creation txn\n") - err = broadcastErr - return - } + fmt.Printf("Creating %v create asset transaction for account %v \n", toCreate, addr) + fmt.Printf("cfg.NumAsset %v, addrAccount.AssetParams %v\n", cfg.NumAsset, addrAccount.AssetParams) - if !cfg.Quiet { - fmt.Printf("Create a new asset: supply=%d, txid=%s\n", totalSupply, txid) - } - accounts[cfg.SrcAccount] -= tx.Fee.Raw - } + // create assets in participant account + for i := 0; i < toCreate; i++ { + var metaLen = 32 + meta := make([]byte, metaLen, metaLen) + crypto.RandBytes(meta[:]) + totalSupply := cfg.MinAccountAsset * uint64(cfg.NumPartAccounts) * 9 + + if totalSupply < cfg.MinAccountAsset { //overflow + fmt.Printf("Too many NumPartAccounts\n") + return + } + assetName := fmt.Sprintf("pong%d", i) + fmt.Printf("Creating asset %s\n", assetName) + tx, createErr := client.MakeUnsignedAssetCreateTx(totalSupply, false, addr, addr, addr, addr, "ping", assetName, "", meta, 0) + if createErr != nil { + fmt.Printf("Cannot make asset create txn with meta %v\n", meta) + err = createErr + return + } + tx, err = client.FillUnsignedTxTemplate(addr, 0, 0, cfg.MaxFee, tx) + if err != nil { + fmt.Printf("Cannot fill asset creation txn\n") + return + } - // get these assets - for { - account, accountErr = client.AccountInformation(cfg.SrcAccount) + _, err = signAndBroadcastTransaction(accounts, addr, tx, client, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "signing and broadcasting asset creation failed with error %v\n", err) + return + } + + totalSent++ + throttleTransactionRate(startTime, cfg, totalSent) + } + account, accountErr := client.AccountInformation(addr) if accountErr != nil { - fmt.Printf("Cannot lookup source account") + fmt.Printf("Cannot lookup source account %v\n", addr) err = accountErr return } - if len(account.AssetParams) >= int(cfg.NumAsset) { - break - } - time.Sleep(time.Second) + + assetParams := account.AssetParams + fmt.Printf("Configured %d assets %+v\n", len(assetParams), assetParams) + } - assetParams = account.AssetParams + time.Sleep(time.Second * 15) + // 2) For each participant account, opt-in to assets of all other participant accounts for addr := range accounts { + fmt.Printf("Opting in assets from account %v\n", addr) addrAccount, addrErr := client.AccountInformation(addr) if addrErr != nil { - fmt.Printf("Cannot lookup source account") + fmt.Printf("Cannot lookup source account\n") err = addrErr return } + assetParams := addrAccount.AssetParams + fmt.Printf("Optining in %d assets %+v\n", len(assetParams), assetParams) + + // Opt-in Accounts for each asset for k := range assetParams { - // if addr already opened this asset, skip - if _, ok := addrAccount.Assets[k]; !ok { - // init asset k in addr - tx, sendErr := client.MakeUnsignedAssetSendTx(k, 0, addr, "", "") - if sendErr != nil { - fmt.Printf("Cannot initiate asset %v in account %v\n", k, addr) - err = sendErr - return + fmt.Printf("optin asset %+v\n", k) + + for addr2 := range accounts { + if addr != addr2 { + fmt.Printf("Opting in assets to account %v \n", addr2) + _, addrErr2 := client.AccountInformation(addr2) + if addrErr2 != nil { + fmt.Printf("Cannot lookup optin account\n") + err = addrErr2 + return + } + + // opt-in asset k for addr + tx, sendErr := client.MakeUnsignedAssetSendTx(k, 0, addr2, "", "") + if sendErr != nil { + fmt.Printf("Cannot initiate asset optin %v in account %v\n", k, addr2) + err = sendErr + return + } + + tx, err = client.FillUnsignedTxTemplate(addr2, 0, 0, cfg.MaxFee, tx) + if err != nil { + fmt.Printf("Cannot fill asset optin %v in account %v\n", k, addr2) + return + } + + _, err = signAndBroadcastTransaction(accounts, addr2, tx, client, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "signing and broadcasting asset optin failed with error %v\n", err) + return + } + + totalSent++ + throttleTransactionRate(startTime, cfg, totalSent) } + } + } + } + time.Sleep(time.Second * 15) - tx, err = client.FillUnsignedTxTemplate(addr, 0, 0, cfg.MaxFee, tx) - if err != nil { - fmt.Printf("Cannot fill asset %v init txn in account %v\n", k, addr) - return - } + // Step 3) Evenly distribute the assets across all participant accounts + for addr := range accounts { + fmt.Printf("Distributing assets from account %v\n", addr) + addrAccount, addrErr := client.AccountInformation(addr) + if addrErr != nil { + fmt.Printf("Cannot lookup source account\n") + err = addrErr + return + } - signedTxn, signErr := client.SignTransactionWithWallet(h, nil, tx) - if signErr != nil { - fmt.Printf("Cannot sign asset %v init txn in account %v\n", k, addr) - err = signErr - return - } + assetParams := addrAccount.AssetParams + fmt.Printf("Distributing %d assets\n", len(assetParams)) - _, broadcastErr := client.BroadcastTransaction(signedTxn) - if broadcastErr != nil { - fmt.Printf("Cannot broadcast asset %v init txn in account %v\n", k, addr) - err = broadcastErr - return - } + // Distribute assets to each account + for k := range assetParams { - if !cfg.Quiet { - fmt.Printf("Init asset %v in account %v\n", k, addr) - } - accounts[addr] -= tx.Fee.Raw - } + fmt.Printf("Distributing asset %v \n", k) - // fund asset - var assetAmt uint64 - if asset, ok := addrAccount.Assets[k]; ok { - if asset.Amount > cfg.MinAccountAsset { - assetAmt = 0 - } else { - assetAmt = cfg.MinAccountAsset - asset.Amount - } - } else { - assetAmt = cfg.MinAccountAsset - } + assetAmt := assetParams[k].Total / uint64(len(accounts)) + for addr2 := range accounts { + if addr != addr2 { - if assetAmt == 0 { - continue - } + fmt.Printf("Distributing assets from %v to %v \n", addr, addr2) - tx, sendErr := constructTxn(cfg.SrcAccount, addr, cfg.MaxFee, assetAmt, k, client, cfg) - if sendErr != nil { - fmt.Printf("Cannot initiate asset %v in account %v\n", k, addr) - err = sendErr - return - } - stxn, signErr := signTxn(cfg.SrcAccount, tx, client, cfg) - if signErr != nil { - fmt.Printf("Cannot sign asset %v init fund txn in account %v\n", k, addr) - err = signErr - return - } - _, broadcastErr := client.BroadcastTransaction(stxn) - if broadcastErr != nil { - fmt.Printf("Cannot broadcast asset %v init fund txn in account %v\n", k, addr) - err = broadcastErr - return - } - if !cfg.Quiet { - fmt.Printf("Fund %d asset %d to account %s\n", assetAmt, k, addr) + tx, sendErr := constructTxn(addr, addr2, cfg.MaxFee, assetAmt, k, client, cfg) + if sendErr != nil { + fmt.Printf("Cannot transfer asset %v from account %v\n", k, addr) + err = sendErr + return + } + + _, err = signAndBroadcastTransaction(accounts, addr, tx, client, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "signing and broadcasting asset distribution failed with error %v\n", err) + return + } + + totalSent++ + throttleTransactionRate(startTime, cfg, totalSent) + } } - accounts[cfg.SrcAccount] -= tx.Fee.Raw + // append the asset to the result assets + resultAssetMaps[k] = assetParams[k] } + } + time.Sleep(time.Second * 10) + return +} +func signAndBroadcastTransaction(accounts map[string]uint64, sender string, tx transactions.Transaction, client libgoal.Client, cfg PpConfig) (txID string, err error) { + var signedTx transactions.SignedTxn + signedTx, err = signTxn(sender, tx, client, cfg) + if err != nil { + fmt.Printf("Cannot sign trx %+v with account %v\nerror %v\n", tx, sender, err) + return } + txID, err = client.BroadcastTransaction(signedTx) + if err != nil { + fmt.Printf("Cannot broadcast transaction %+v\nerror %v \n", signedTx, err) + return + } + if !cfg.Quiet { + fmt.Printf("Broadcast transaction %v\n", txID) + } + accounts[sender] -= tx.Fee.Raw + return } diff --git a/shared/pingpong/pingpong.go b/shared/pingpong/pingpong.go index 5c717fa63c..fb1cefef44 100644 --- a/shared/pingpong/pingpong.go +++ b/shared/pingpong/pingpong.go @@ -25,7 +25,7 @@ import ( "time" "github.com/algorand/go-algorand/crypto" - v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/libgoal" @@ -36,41 +36,119 @@ func PrepareAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]u cfg = initCfg accounts, cfg, err = ensureAccounts(ac, cfg) if err != nil { - return - } - - err = fundAccounts(accounts, ac, cfg) - if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "ensure accounts failed %v\n", err) return } if cfg.NumAsset > 0 { - assetParams, err = prepareAssets(accounts, ac, cfg) + + // zero out max amount for asset transactions + cfg.MaxAmt = 0 + + wallet, walletErr := ac.GetUnencryptedWalletHandle() if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to access wallet %v\n", walletErr) + err = walletErr return } + fmt.Printf("Generating %v new accounts for asset transfer test\n", cfg.NumPartAccounts) + // remove existing accounts except for src account + for k := range accounts { + if k != cfg.SrcAccount { + delete(accounts, k) + } + } + // create new accounts for asset testing + assetAccounts := make(map[string]uint64) + assetAccounts, err = generateAccounts(ac, assetAccounts, cfg.NumPartAccounts-1, wallet) + + for addr := range assetAccounts { + fmt.Printf("generated account %v\n", addr) + } + + for k := range assetAccounts { + accounts[k] = assetAccounts[k] + } + err = fundAccounts(accounts, ac, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "fund accounts failed %v\n", err) + return + } + + assetParams, err = prepareAssets(assetAccounts, ac, cfg) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "prepare assets failed %v\n", err) + return + } + + for k := range assetAccounts { + accounts[k] = assetAccounts[k] + } + } + + for addr := range accounts { + fmt.Printf("**** participant account %v\n", addr) } - // we need to fund Accounts again since prepareAssets spends err = fundAccounts(accounts, ac, cfg) if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "fund accounts failed %v\n", err) return } return } +// determine the min balance per participant account +func computeAccountMinBalance(cfg PpConfig) (requiredBalance uint64) { + const minActiveAccountBalance uint64 = 100000 // min balance for any active account + + var fee uint64 = 1000 + if cfg.MinFee > fee { + fee = cfg.MinFee + } + if cfg.MaxFee != 0 { + fee = cfg.MaxFee + } + requiredBalance = minActiveAccountBalance + + // add cost of assets + if cfg.NumAsset > 0 { + assetCost := minActiveAccountBalance*uint64(cfg.NumAsset)*uint64(cfg.NumPartAccounts) + // assets*accounts + (fee)*uint64(cfg.NumAsset) + // asset creations + (fee)*uint64(cfg.NumAsset)*uint64(cfg.NumPartAccounts) + // asset opt-ins + (fee)*uint64(cfg.NumAsset)*uint64(cfg.NumPartAccounts) // asset distributions + requiredBalance += assetCost + } + // add cost of transactions + requiredBalance += (cfg.MaxAmt + fee) * 2 * cfg.TxnPerSec * uint64(math.Ceil(cfg.RefreshTime.Seconds())) + + // override computed value if less than configured value + if cfg.MinAccountFunds > requiredBalance { + requiredBalance = cfg.MinAccountFunds + } + + return +} + func fundAccounts(accounts map[string]uint64, client libgoal.Client, cfg PpConfig) error { srcFunds := accounts[cfg.SrcAccount] + startTime := time.Now() + var totalSent uint64 + // Fee of 0 will make cause the function to use the suggested one by network fee := uint64(0) - minFund := cfg.MinAccountFunds + (cfg.MaxAmt+cfg.MaxFee)*cfg.TxnPerSec*uint64(math.Ceil(cfg.RefreshTime.Seconds())) + + minFund := computeAccountMinBalance(cfg) + + fmt.Printf("adjusting account balance to %d\n", minFund) for addr, balance := range accounts { + fmt.Printf("adjusting balance of account %v\n", addr) if balance < minFund { toSend := minFund - balance if srcFunds <= toSend { - return fmt.Errorf("source account has insufficient funds %d - needs %d", srcFunds, toSend) + return fmt.Errorf("source account %s has insufficient funds %d - needs %d", cfg.SrcAccount, srcFunds, toSend) } srcFunds -= toSend _, err := client.SendPaymentFromUnencryptedWallet(cfg.SrcAccount, addr, fee, toSend, nil) @@ -78,6 +156,9 @@ func fundAccounts(accounts map[string]uint64, client libgoal.Client, cfg PpConfi return err } accounts[addr] = minFund + + totalSent++ + throttleTransactionRate(startTime, cfg, totalSent) } } return nil @@ -87,6 +168,7 @@ func refreshAccounts(accounts map[string]uint64, client libgoal.Client, cfg PpCo for addr := range accounts { amount, err := client.GetBalance(addr) if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "error refreshAccounts: %v\n", err) return err } @@ -140,6 +222,7 @@ func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uin for { if ctx.Err() != nil { + _, _ = fmt.Fprintf(os.Stderr, "error bad context in RunPingPong: %v\n", ctx.Err()) break } startTime := time.Now() @@ -147,6 +230,7 @@ func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uin var totalSent, totalSucceeded uint64 for !time.Now().After(stopTime) { + fromList := listSufficientAccounts(accounts, cfg.MinAccountFunds+(cfg.MaxAmt+cfg.MaxFee)*2, cfg.SrcAccount) toList := listSufficientAccounts(accounts, 0, cfg.SrcAccount) @@ -154,30 +238,25 @@ func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uin totalSent += sent totalSucceeded += succeded if err != nil { - fmt.Fprintf(os.Stderr, "error sending transactions: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "error sending transactions: %v\n", err) } if cfg.RefreshTime > 0 && time.Now().After(refreshTime) { err = refreshAccounts(accounts, ac, cfg) if err != nil { - fmt.Fprintf(os.Stderr, "error refreshing: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "error refreshing: %v\n", err) } refreshTime = refreshTime.Add(cfg.RefreshTime) } - localTimeDelta := time.Now().Sub(startTime) - currentTps := float64(totalSent) / localTimeDelta.Seconds() - if currentTps > float64(cfg.TxnPerSec) { - sleepSec := float64(totalSent)/float64(cfg.TxnPerSec) - localTimeDelta.Seconds() - sleepTime := time.Duration(int64(math.Round(sleepSec*1000))) * time.Millisecond - time.Sleep(sleepTime) - } + throttleTransactionRate(startTime, cfg, totalSent) } + timeDelta := time.Now().Sub(startTime) - fmt.Fprintf(os.Stdout, "Sent %d transactions (%d attempted) in %d seconds\n", totalSucceeded, totalSent, int(math.Round(timeDelta.Seconds()))) + _, _ = fmt.Fprintf(os.Stdout, "Sent %d transactions (%d attempted) in %d seconds\n", totalSucceeded, totalSent, int(math.Round(timeDelta.Seconds()))) if cfg.RestTime > 0 { - fmt.Fprintf(os.Stdout, "Pausing %d seconds before sending more transactions\n", int(math.Round(cfg.RestTime.Seconds()))) + _, _ = fmt.Fprintf(os.Stdout, "Pausing %d seconds before sending more transactions\n", int(math.Round(cfg.RestTime.Seconds()))) time.Sleep(restTime) } } @@ -229,12 +308,13 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara txn, consErr := constructTxn(from, to, fee, amt, assetID, client, cfg) if consErr != nil { err = consErr + _, _ = fmt.Fprintf(os.Stderr, "constructTxn failed: %v\n", err) return } // would we have enough money after taking into account the current updated fees ? if accounts[from] <= (txn.Fee.Raw + amt + cfg.MinAccountFunds) { - fmt.Fprintf(os.Stdout, "Skipping sending %d : %s -> %s; Current cost too high.\n", amt, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Skipping sending %d : %s -> %s; Current cost too high.\n", amt, from, to) continue } fromBalanceChange = -int64(txn.Fee.Raw + amt) @@ -244,6 +324,7 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara stxn, signErr := signTxn(from, txn, client, cfg) if signErr != nil { err = signErr + _, _ = fmt.Fprintf(os.Stderr, "signTxn failed: %v\n", err) return } @@ -264,6 +345,7 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara fromBalanceChange += int64(amt) } if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "group tx failed: %v\n", err) return } txGroup = append(txGroup, txn) @@ -271,11 +353,11 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara // would we have enough money after taking into account the current updated fees ? if int64(accounts[from])+fromBalanceChange <= int64(cfg.MinAccountFunds) { - fmt.Fprintf(os.Stdout, "Skipping sending %d : %s -> %s; Current cost too high.\n", amt, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Skipping sending %d : %s -> %s; Current cost too high.\n", amt, from, to) continue } if int64(accounts[to])+toBalanceChange <= int64(cfg.MinAccountFunds) { - fmt.Fprintf(os.Stdout, "Skipping sending back %d : %s -> %s; Current cost too high.\n", amt, to, from) + _, _ = fmt.Fprintf(os.Stdout, "Skipping sending back %d : %s -> %s; Current cost too high.\n", amt, to, from) continue } @@ -287,7 +369,7 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara } if !cfg.Quiet { - fmt.Fprintf(os.Stdout, "Sending TxnGroup: ID %v, size %v \n", gid, len(txGroup)) + _, _ = fmt.Fprintf(os.Stdout, "Sending TxnGroup: ID %v, size %v \n", gid, len(txGroup)) } // Sign each transaction @@ -313,14 +395,16 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara } if sendErr != nil && !cfg.Quiet { - fmt.Fprintf(os.Stderr, "error sending transaction: %v\n", sendErr) + _, _ = fmt.Fprintf(os.Stderr, "error sending transaction: %v\n", sendErr) } else { successCount++ accounts[from] = uint64(fromBalanceChange + int64(accounts[from])) accounts[to] = uint64(toBalanceChange + int64(accounts[to])) } if sendErr != nil { + _, _ = fmt.Fprintf(os.Stderr, "error sending Transaction, sleeping .5 seconds: %+v\n", sendErr) err = sendErr + time.Sleep(500 * time.Millisecond) return } if cfg.DelayBetweenTxn > 0 { @@ -354,26 +438,41 @@ func constructTxn(from, to string, fee, amt, assetID uint64, client libgoal.Clie if cfg.NumAsset == 0 { // Construct payment transaction txn, err = client.ConstructPayment(from, to, fee, amt, noteField[:], "", lease, 0, 0) if !cfg.Quiet { - fmt.Fprintf(os.Stdout, "Sending %d : %s -> %s\n", amt, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Sending %d : %s -> %s\n", amt, from, to) } } else { // Construct asset transaction txn, err = client.MakeUnsignedAssetSendTx(assetID, amt, to, "", "") if err != nil { + _, _ = fmt.Fprintf(os.Stdout, "error making unsigned asset send tx %v\n", err) return } txn.Note = noteField[:] txn.Lease = lease txn, err = client.FillUnsignedTxTemplate(from, 0, 0, cfg.MaxFee, txn) if !cfg.Quiet { - fmt.Fprintf(os.Stdout, "Sending %d asset %d: %s -> %s\n", amt, assetID, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Sending %d asset %d: %s -> %s\n", amt, assetID, from, to) } } if err != nil { + _, _ = fmt.Fprintf(os.Stdout, "error constructing transaction %v\n", err) return } // adjust transaction duration for 5 rounds. That would prevent it from getting stuck in the transaction pool for too long. txn.LastValid = txn.FirstValid + 5 + + // if cfg.MaxFee == 0, automatically adjust the fee amount to required min fee + if cfg.MaxFee == 0 { + var suggestedFee uint64 + suggestedFee, err = client.SuggestedFee() + if err != nil { + _, _ = fmt.Fprintf(os.Stdout, "error retrieving suggestedFee: %v\n", err) + return + } + if suggestedFee > txn.Fee.Raw { + txn.Fee.Raw = suggestedFee + } + } return } From cb1758ec40801f425f210901c7a915833eeb8099 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 11 Jun 2020 13:34:24 -0400 Subject: [PATCH 045/267] Add a more restrictive timeout for block fetching (#1156) The timeouts values used by the fast catchup service doesn't handle failure cases well. Since these are hard-coded, we can't really address any field-deployment issue by tweaking the configuration settings. This PR moves the hard-coded values to the configuration file, along with providing more robust default values. These should allow us to adjust them dynamically if (and when) needed. --- catchup/catchpointService.go | 46 +++++++------- catchup/fetcher.go | 13 ++-- catchup/fetcher_test.go | 14 +++-- catchup/httpFetcher.go | 14 +++-- catchup/service.go | 6 +- catchup/service_test.go | 2 +- catchup/wsFetcher.go | 15 +++-- config/config.go | 12 ++++ config/config_test.go | 7 ++- config/local_defaults.go | 89 +++++++++++++++++++++++++++- installer/config.json.example | 6 +- node/node.go | 4 +- test/testdata/configs/config-v9.json | 58 ++++++++++++++++++ 13 files changed, 230 insertions(+), 56 deletions(-) create mode 100644 test/testdata/configs/config-v9.json diff --git a/catchup/catchpointService.go b/catchup/catchpointService.go index daa622c5f7..577ee8e754 100644 --- a/catchup/catchpointService.go +++ b/catchup/catchpointService.go @@ -66,19 +66,11 @@ type CatchpointCatchupService struct { ledger *ledger.Ledger // lastBlockHeader is the latest block we have before going into catchpoint catchup mode. We use it to serve the node status requests instead of going to the ledger. lastBlockHeader bookkeeping.BlockHeader + config config.Local } -const ( - // maxLedgerDownloadAttempts is the number of attempts the catchpoint file will be downloaded and verified. Once we pass that number of attempts - // the catchpoint catchup is considered to be a failuire, and the node would resume to work as before. - maxLedgerDownloadAttempts = 50 - // maxBlockDownloadAttempts is the number of block download attempts failuires allows while downloading the blocks. Once we pass that number of attempts - // the catchpoint catchup is considered to be a failuire, and the node would resume to work as before. - maxBlockDownloadAttempts = 50 -) - // MakeResumedCatchpointCatchupService creates a catchpoint catchup service for a node that is already in catchpoint catchup mode -func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (service *CatchpointCatchupService, err error) { +func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger, cfg config.Local) (service *CatchpointCatchupService, err error) { service = &CatchpointCatchupService{ stats: CatchpointCatchupStats{ StartTime: time.Now(), @@ -89,6 +81,7 @@ func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCat newService: false, net: net, ledger: l, + config: cfg, } service.lastBlockHeader, err = l.BlockHdr(l.Latest()) if err != nil { @@ -103,7 +96,7 @@ func MakeResumedCatchpointCatchupService(ctx context.Context, node CatchpointCat } // MakeNewCatchpointCatchupService creates a new catchpoint catchup service for a node that is not in catchpoint catchup mode -func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger) (service *CatchpointCatchupService, err error) { +func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNodeServices, log logging.Logger, net network.GossipNode, l *ledger.Ledger, cfg config.Local) (service *CatchpointCatchupService, err error) { if catchpoint == "" { return nil, fmt.Errorf("MakeNewCatchpointCatchupService: catchpoint is invalid") } @@ -119,6 +112,7 @@ func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNo newService: true, net: net, ledger: l, + config: cfg, } service.lastBlockHeader, err = l.BlockHdr(l.Latest()) if err != nil { @@ -262,7 +256,7 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) { return cs.abort(err) // we want to keep it with the context error. } - if attemptsCount >= maxLedgerDownloadAttempts { + if attemptsCount >= cs.config.CatchupLedgerDownloadRetryAttempts { err = fmt.Errorf("catchpoint catchup exceeded number of attempts to retrieve ledger") return cs.abort(err) } @@ -283,7 +277,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro return cs.abort(fmt.Errorf("processStageLastestBlockDownload failed to retrieve catchup block round : %v", err)) } - fetcherFactory := MakeNetworkFetcherFactory(cs.net, 10, nil) + fetcherFactory := MakeNetworkFetcherFactory(cs.net, 10, nil, &cs.config) attemptsCount := 0 var blk *bookkeeping.Block var client FetcherClient @@ -301,7 +295,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro if err == cs.ctx.Err() { return cs.abort(err) } - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -316,7 +310,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro if _, ok := config.Consensus[blk.BlockHeader.CurrentProtocol]; !ok { cs.log.Warnf("processStageLastestBlockDownload: unsupported protocol version detected: '%v'", blk.BlockHeader.CurrentProtocol) - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -328,7 +322,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro if !blk.ContentsMatchHeader() { cs.log.Warnf("processStageLastestBlockDownload: downloaded block content does not match downloaded block header") - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -342,7 +336,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro if err == cs.ctx.Err() { return cs.abort(err) } - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -352,7 +346,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro err = cs.ledgerAccessor.StoreBalancesRound(cs.ctx, blk) if err != nil { - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -362,7 +356,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro err = cs.ledgerAccessor.StoreFirstBlock(cs.ctx, blk) if err != nil { - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -372,7 +366,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro err = cs.updateStage(ledger.CatchpointCatchupStateBlocksDownload) if err != nil { - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. blk = nil continue @@ -411,7 +405,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { cs.statsMu.Unlock() prevBlock := &topBlock - fetcherFactory := MakeNetworkFetcherFactory(cs.net, 10, nil) + fetcherFactory := MakeNetworkFetcherFactory(cs.net, 10, nil, &cs.config) blocksFetched := uint64(1) // we already got the first block in the previous step. var blk *bookkeeping.Block var client FetcherClient @@ -440,7 +434,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { if err == cs.ctx.Err() { return cs.abort(err) } - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. continue } @@ -457,7 +451,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { // not identical, retry download. cs.log.Warnf("processStageBlocksDownload downloaded block(%d) did not match it's successor(%d) block hash %v != %v", blk.Round(), prevBlock.Round(), blk.Hash(), prevBlock.BlockHeader.Branch) cs.updateBlockRetrievalStatistics(-1, 0) - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. continue } @@ -468,7 +462,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { if _, ok := config.Consensus[blk.BlockHeader.CurrentProtocol]; !ok { cs.log.Warnf("processStageBlocksDownload: unsupported protocol version detected: '%v'", blk.BlockHeader.CurrentProtocol) cs.updateBlockRetrievalStatistics(-1, 0) - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. continue } @@ -480,7 +474,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { cs.log.Warnf("processStageBlocksDownload: downloaded block content does not match downloaded block header") // try again. cs.updateBlockRetrievalStatistics(-1, 0) - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. continue } @@ -494,7 +488,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { if err != nil { cs.log.Warnf("processStageBlocksDownload failed to store downloaded staging block for round %d", blk.Round()) cs.updateBlockRetrievalStatistics(-1, -1) - if attemptsCount <= maxBlockDownloadAttempts { + if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. continue } diff --git a/catchup/fetcher.go b/catchup/fetcher.go index 15e4b009a0..34ca8a92d9 100644 --- a/catchup/fetcher.go +++ b/catchup/fetcher.go @@ -21,11 +21,11 @@ import ( "errors" "fmt" "math/rand" - "time" "github.com/algorand/go-deadlock" "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" @@ -34,9 +34,6 @@ import ( "github.com/algorand/go-algorand/rpcs" ) -// DefaultFetchTimeout is the default time a fetcher should wait for a block -const DefaultFetchTimeout = 20 * time.Second // TODO this should be in config - // Fetcher queries the current block of the network, and fetches agreed-upon blocks type Fetcher interface { // FetchBlock fetches a block for a given round. @@ -65,6 +62,7 @@ type NetworkFetcherFactory struct { net network.GossipNode peerLimit int fs *rpcs.WsFetcherService + cfg *config.Local log logging.Logger } @@ -72,7 +70,7 @@ type NetworkFetcherFactory struct { func (factory NetworkFetcherFactory) makeHTTPFetcherFromPeer(log logging.Logger, peer network.Peer) FetcherClient { hp, ok := peer.(network.HTTPPeer) if ok { - return MakeHTTPFetcher(log, hp, factory.net) + return MakeHTTPFetcher(log, hp, factory.net, factory.cfg) } log.Errorf("%T %#v is not HTTPPeer", peer, peer) return nil @@ -80,12 +78,13 @@ func (factory NetworkFetcherFactory) makeHTTPFetcherFromPeer(log logging.Logger, // MakeNetworkFetcherFactory returns a network fetcher factory, that associates fetchers with no more than peerLimit peers from the aggregator. // WSClientSource can be nil, if no network exists to create clients from (defaults to http clients) -func MakeNetworkFetcherFactory(net network.GossipNode, peerLimit int, fs *rpcs.WsFetcherService) NetworkFetcherFactory { +func MakeNetworkFetcherFactory(net network.GossipNode, peerLimit int, fs *rpcs.WsFetcherService, cfg *config.Local) NetworkFetcherFactory { var factory NetworkFetcherFactory factory.net = net factory.peerLimit = peerLimit factory.log = logging.Base() factory.fs = fs + factory.cfg = cfg return factory } @@ -132,7 +131,7 @@ func (factory NetworkFetcherFactory) NewOverGossip(tag protocol.Tag) Fetcher { factory.log.Info("WsFetcherService not available; fetch over gossip disabled") return factory.New() } - f := MakeWsFetcher(factory.log, tag, gossipPeers, factory.fs) + f := MakeWsFetcher(factory.log, tag, gossipPeers, factory.fs, factory.cfg) return &ComposedFetcher{fetchers: []Fetcher{factory.New(), f}} } diff --git a/catchup/fetcher_test.go b/catchup/fetcher_test.go index ae228853e9..7a844eb87a 100644 --- a/catchup/fetcher_test.go +++ b/catchup/fetcher_test.go @@ -133,7 +133,8 @@ func getAllClientsSelectedForRound(t *testing.T, fetcher *NetworkFetcher, round func TestSelectValidRemote(t *testing.T) { network := makeMockClientAggregator(t, false, false) - factory := MakeNetworkFetcherFactory(network, numberOfPeers, nil) + cfg := config.GetDefaultLocal() + factory := MakeNetworkFetcherFactory(network, numberOfPeers, nil, &cfg) factory.log = logging.TestingLog(t) fetcher := factory.New() require.Equal(t, numberOfPeers, len(fetcher.(*NetworkFetcher).peers)) @@ -600,7 +601,8 @@ func TestGetBlockHTTP(t *testing.T) { net.addPeer(rootURL) _, ok := net.GetPeers(network.PeersConnectedOut)[0].(network.HTTPPeer) require.True(t, ok) - factory := MakeNetworkFetcherFactory(net, numberOfPeers, nil) + cfg := config.GetDefaultLocal() + factory := MakeNetworkFetcherFactory(net, numberOfPeers, nil, &cfg) factory.log = logging.TestingLog(t) fetcher := factory.New() // we have one peer, the HTTP block server @@ -713,7 +715,7 @@ func TestGetBlockMocked(t *testing.T) { require.NoError(t, ledgerA.AddBlock(b, agreement.Certificate{Round: next})) // B tries to fetch block - factory := MakeNetworkFetcherFactory(nodeB, 10, nil) + factory := MakeNetworkFetcherFactory(nodeB, 10, nil, &cfg) factory.log = logging.TestingLog(t) nodeBRPC := factory.New() ctx, cf := context.WithTimeout(context.Background(), time.Second) @@ -763,7 +765,7 @@ func TestGetFutureBlock(t *testing.T) { rpcs.MakeBlockService(config.GetDefaultLocal(), ledgerA, nodeA, "test genesisID") // B tries to fetch block 4 - factory := MakeNetworkFetcherFactory(nodeB, 10, nil) + factory := MakeNetworkFetcherFactory(nodeB, 10, nil, &cfg) factory.log = logging.TestingLog(t) nodeBRPC := factory.New() ctx, cf := context.WithTimeout(context.Background(), time.Second) @@ -869,6 +871,8 @@ func TestGetBlockWS(t *testing.T) { return } + cfg := config.GetDefaultLocal() + versions := []string{"1", "2.1"} for _, version := range versions { // range network.SupportedProtocolVersions { @@ -888,7 +892,7 @@ func TestGetBlockWS(t *testing.T) { _, ok := net.GetPeers(network.PeersConnectedIn)[0].(network.UnicastPeer) require.True(t, ok) - factory := MakeNetworkFetcherFactory(net, numberOfPeers, fs) + factory := MakeNetworkFetcherFactory(net, numberOfPeers, fs, &cfg) factory.log = logging.TestingLog(t) fetcher := factory.NewOverGossip(protocol.UniCatchupReqTag) // we have one peer, the Ws block server diff --git a/catchup/httpFetcher.go b/catchup/httpFetcher.go index 4aa9d2ed7f..227127e334 100644 --- a/catchup/httpFetcher.go +++ b/catchup/httpFetcher.go @@ -23,7 +23,9 @@ import ( "net/http" "path" "strconv" + "time" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/network" @@ -50,17 +52,19 @@ type HTTPFetcher struct { client *http.Client - log logging.Logger + log logging.Logger + config *config.Local } // MakeHTTPFetcher wraps an HTTPPeer so that we can get blocks from it -func MakeHTTPFetcher(log logging.Logger, peer network.HTTPPeer, net network.GossipNode) (fc FetcherClient) { +func MakeHTTPFetcher(log logging.Logger, peer network.HTTPPeer, net network.GossipNode, cfg *config.Local) (fc FetcherClient) { fc = &HTTPFetcher{ peer: peer, rootURL: peer.GetAddress(), net: net, client: peer.GetHTTPClient(), - log: log} + log: log, + config: cfg} return } @@ -78,7 +82,9 @@ func (hf *HTTPFetcher) GetBlockBytes(ctx context.Context, r basics.Round) (data if err != nil { return nil, err } - request = request.WithContext(ctx) + requestCtx, requestCancel := context.WithTimeout(ctx, time.Duration(hf.config.CatchupHTTPBlockFetchTimeoutSec)*time.Second) + defer requestCancel() + request = request.WithContext(requestCtx) network.SetUserAgentHeader(request.Header) response, err := hf.client.Do(request) if err != nil { diff --git a/catchup/service.go b/catchup/service.go index 58773bddb0..d580d3992c 100644 --- a/catchup/service.go +++ b/catchup/service.go @@ -102,12 +102,12 @@ func MakeService(log logging.Logger, config config.Local, net network.GossipNode s = &Service{} s.cfg = config - s.fetcherFactory = MakeNetworkFetcherFactory(net, catchupPeersForSync, wsf) + s.fetcherFactory = MakeNetworkFetcherFactory(net, catchupPeersForSync, wsf, &config) s.ledger = ledger s.net = net s.auth = auth s.unmatchedPendingCertificates = unmatchedPendingCertificates - s.latestRoundFetcherFactory = MakeNetworkFetcherFactory(net, blockQueryPeerLimit, wsf) + s.latestRoundFetcherFactory = MakeNetworkFetcherFactory(net, blockQueryPeerLimit, wsf, &config) s.log = log.With("Context", "sync") s.parallelBlocks = config.CatchupParallelBlocks s.deadlineTimeout = agreement.DeadlineTimeout() @@ -152,7 +152,7 @@ func (s *Service) SynchronizingTime() time.Duration { // function scope to make a bunch of defer statements better func (s *Service) innerFetch(fetcher Fetcher, r basics.Round) (blk *bookkeeping.Block, cert *agreement.Certificate, rpcc FetcherClient, err error) { - ctx, cf := context.WithTimeout(s.ctx, DefaultFetchTimeout) + ctx, cf := context.WithCancel(s.ctx) defer cf() stopWaitingForLedgerRound := make(chan struct{}) defer close(stopWaitingForLedgerRound) diff --git a/catchup/service_test.go b/catchup/service_test.go index 41a7ba9a84..090ce3c212 100644 --- a/catchup/service_test.go +++ b/catchup/service_test.go @@ -108,7 +108,7 @@ type MockedFetcher struct { func (m *MockedFetcher) FetchBlock(ctx context.Context, round basics.Round) (*bookkeeping.Block, *agreement.Certificate, FetcherClient, error) { if m.timeout { - time.Sleep(DefaultFetchTimeout + time.Second) + time.Sleep(time.Duration(config.GetDefaultLocal().CatchupHTTPBlockFetchTimeoutSec)*time.Second + time.Second) } time.Sleep(m.latency) diff --git a/catchup/wsFetcher.go b/catchup/wsFetcher.go index ef3cc3f4a8..e27b3add94 100644 --- a/catchup/wsFetcher.go +++ b/catchup/wsFetcher.go @@ -19,10 +19,12 @@ package catchup import ( "context" "fmt" + "time" "github.com/algorand/go-deadlock" "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" @@ -42,6 +44,7 @@ type WsFetcher struct { f *NetworkFetcher clients map[network.Peer]*wsFetcherClient + config *config.Local // service service *rpcs.WsFetcherService @@ -54,10 +57,11 @@ type WsFetcher struct { // MakeWsFetcher creates a fetcher that fetches over the gossip network. // It instantiates a NetworkFetcher under the hood, registers as a handler for the given message tag, // and demuxes messages appropriately to the corresponding fetcher clients. -func MakeWsFetcher(log logging.Logger, tag protocol.Tag, peers []network.Peer, service *rpcs.WsFetcherService) Fetcher { +func MakeWsFetcher(log logging.Logger, tag protocol.Tag, peers []network.Peer, service *rpcs.WsFetcherService, cfg *config.Local) Fetcher { f := &WsFetcher{ - log: log, - tag: tag, + log: log, + tag: tag, + config: cfg, } f.clients = make(map[network.Peer]*wsFetcherClient) p := make([]FetcherClient, len(peers)) @@ -67,6 +71,7 @@ func MakeWsFetcher(log logging.Logger, tag protocol.Tag, peers []network.Peer, s tag: f.tag, pendingCtxs: make(map[context.Context]context.CancelFunc), service: service, + config: cfg, } p[i] = fc f.clients[peer] = fc @@ -107,6 +112,7 @@ type wsFetcherClient struct { tag protocol.Tag // the tag that is associated with the request/ service *rpcs.WsFetcherService // the fetcher service. This is where we perform the actual request and waiting for the response. pendingCtxs map[context.Context]context.CancelFunc // a map of all the current pending contexts. + config *config.Local closed bool // a flag indicating that the fetcher will not perform additional block retrivals. @@ -120,7 +126,8 @@ func (w *wsFetcherClient) GetBlockBytes(ctx context.Context, r basics.Round) ([] if w.closed { return nil, fmt.Errorf("wsFetcherClient(%d): shutdown", r) } - childCtx, cancelFunc := context.WithCancel(ctx) + + childCtx, cancelFunc := context.WithTimeout(ctx, time.Duration(w.config.CatchupGossipBlockFetchTimeoutSec)*time.Second) w.pendingCtxs[childCtx] = cancelFunc w.mu.Unlock() diff --git a/config/config.go b/config/config.go index afe5f21879..a84f53dc90 100644 --- a/config/config.go +++ b/config/config.go @@ -292,6 +292,18 @@ type Local struct { // EnableGossipBlockService enables the block serving service over the gossip network. The functionality of this depends on NetAddress, which must also be provided. // This functionality is required for the relays to perform catchup from nodes. EnableGossipBlockService bool + + // CatchupHTTPBlockFetchTimeoutSec controls how long the http query for fetching a block from a relay would take before giving up and trying another relay. + CatchupHTTPBlockFetchTimeoutSec int + + // CatchupGossipBlockFetchTimeoutSec controls how long the gossip query for fetching a block from a relay would take before giving up and trying another relay. + CatchupGossipBlockFetchTimeoutSec int + + // CatchupLedgerDownloadRetryAttempts controls the number of attempt the ledger fetching would be attempted before giving up catching up to the provided catchpoint. + CatchupLedgerDownloadRetryAttempts int + + // CatchupLedgerDownloadRetryAttempts controls the number of attempt the block fetching would be attempted before giving up catching up to the provided catchpoint. + CatchupBlockDownloadRetryAttempts int } // Filenames of config files within the configdir (e.g. ~/.algorand) diff --git a/config/config_test.go b/config/config_test.go index 7f89a8fb38..213db2cba7 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -314,7 +314,7 @@ func TestConfigMigrateFromDisk(t *testing.T) { func TestConfigInvariant(t *testing.T) { a := require.New(t) - a.Equal(uint32(8), configVersion, "If you bump Config Version, please update this test (and consider if you should be adding more)") + a.Equal(uint32(9), configVersion, "If you bump Config Version, please update this test (and consider if you should be adding more)") ourPath, err := os.Getwd() a.NoError(err) @@ -364,6 +364,11 @@ func TestConfigInvariant(t *testing.T) { err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v8.json"), &c8) a.NoError(err) a.Equal(defaultLocalV8, c8) + + c9 := Local{} + err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v9.json"), &c9) + a.NoError(err) + a.Equal(defaultLocalV9, c9) } func TestConfigLatestVersion(t *testing.T) { diff --git a/config/local_defaults.go b/config/local_defaults.go index bf65ad1244..280d9bd7b8 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -21,9 +21,9 @@ import ( "time" ) -var defaultLocal = defaultLocalV8 +var defaultLocal = defaultLocalV9 -const configVersion = uint32(8) +const configVersion = uint32(9) // !!! WARNING !!! // @@ -39,6 +39,70 @@ const configVersion = uint32(8) // // !!! WARNING !!! +var defaultLocalV9 = Local{ + // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file + Version: 9, + Archival: false, + BaseLoggerDebugLevel: 4, + BroadcastConnectionsLimit: -1, + AnnounceParticipationKey: true, + PriorityPeers: map[string]bool{}, + CadaverSizeTarget: 1073741824, + CatchupFailurePeerRefreshRate: 10, + CatchupParallelBlocks: 16, + ConnectionsRateLimitingCount: 60, + ConnectionsRateLimitingWindowSeconds: 1, + DeadlockDetection: 0, + DNSBootstrapID: ".algorand.network", + EnableAgreementReporting: false, + EnableAgreementTimeMetrics: false, + EnableIncomingMessageFilter: false, + EnableMetricReporting: false, + EnableOutgoingNetworkMessageFiltering: true, + EnableRequestLogger: false, + EnableTopAccountsReporting: false, + EndpointAddress: "127.0.0.1:0", + GossipFanout: 4, + IncomingConnectionsLimit: 10000, + IncomingMessageFilterBucketCount: 5, + IncomingMessageFilterBucketSize: 512, + LogArchiveName: "node.archive.log", + LogArchiveMaxAge: "", + LogSizeLimit: 1073741824, + MaxConnectionsPerIP: 30, + NetAddress: "", + NodeExporterListenAddress: ":9100", + NodeExporterPath: "./node_exporter", + OutgoingMessageFilterBucketCount: 3, + OutgoingMessageFilterBucketSize: 128, + ReconnectTime: 1 * time.Minute, + ReservedFDs: 256, + RestReadTimeoutSeconds: 15, + RestWriteTimeoutSeconds: 120, + RunHosted: false, + SuggestedFeeBlockHistory: 3, + SuggestedFeeSlidingWindowSize: 50, + TelemetryToLog: true, + TxPoolExponentialIncreaseFactor: 2, + TxPoolSize: 15000, + TxSyncIntervalSeconds: 60, + TxSyncTimeoutSeconds: 30, + TxSyncServeResponseSize: 1000000, + PeerConnectionsUpdateInterval: 3600, + DNSSecurityFlags: 0x01, + EnablePingHandler: true, + CatchpointInterval: 10000, + CatchpointFileHistoryLength: 365, + EnableLedgerService: false, + EnableBlockService: false, + EnableGossipBlockService: true, + CatchupHTTPBlockFetchTimeoutSec: 4, // added in V9 + CatchupGossipBlockFetchTimeoutSec: 4, // added in V9 + CatchupLedgerDownloadRetryAttempts: 50, // added in V9 + CatchupBlockDownloadRetryAttempts: 1000, // added in V9 + // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file +} + var defaultLocalV8 = Local{ // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file Version: 8, @@ -566,6 +630,27 @@ func migrate(cfg Local) (newCfg Local, err error) { newCfg.Version = 8 } + // Migrate 8 -> 9 + if newCfg.Version == 8 { + if newCfg.CatchupHTTPBlockFetchTimeoutSec == defaultLocalV8.CatchupHTTPBlockFetchTimeoutSec { + newCfg.CatchupHTTPBlockFetchTimeoutSec = defaultLocalV9.CatchupHTTPBlockFetchTimeoutSec + } + + if newCfg.CatchupGossipBlockFetchTimeoutSec == defaultLocalV8.CatchupGossipBlockFetchTimeoutSec { + newCfg.CatchupGossipBlockFetchTimeoutSec = defaultLocalV9.CatchupGossipBlockFetchTimeoutSec + } + + if newCfg.CatchupLedgerDownloadRetryAttempts == defaultLocalV8.CatchupLedgerDownloadRetryAttempts { + newCfg.CatchupLedgerDownloadRetryAttempts = defaultLocalV9.CatchupLedgerDownloadRetryAttempts + } + + if newCfg.CatchupBlockDownloadRetryAttempts == defaultLocalV8.CatchupBlockDownloadRetryAttempts { + newCfg.CatchupBlockDownloadRetryAttempts = defaultLocalV9.CatchupBlockDownloadRetryAttempts + } + + newCfg.Version = 9 + } + if newCfg.Version != configVersion { err = fmt.Errorf("failed to migrate config version %d (stuck at %d) to latest %d", cfg.Version, newCfg.Version, configVersion) } diff --git a/installer/config.json.example b/installer/config.json.example index 1df169add2..18ae2d952e 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -1,11 +1,15 @@ { - "Version": 8, + "Version": 9, "AnnounceParticipationKey": true, "Archival": false, "BaseLoggerDebugLevel": 4, "BroadcastConnectionsLimit": -1, "CadaverSizeTarget": 1073741824, + "CatchupBlockDownloadRetryAttempts": 1000, "CatchupFailurePeerRefreshRate": 10, + "CatchupGossipBlockFetchTimeoutSec": 4, + "CatchupHTTPBlockFetchTimeoutSec": 4, + "CatchupLedgerDownloadRetryAttempts": 50, "CatchupParallelBlocks": 16, "CatchpointInterval": 10000, "CatchpointFileHistoryLength": 365, diff --git a/node/node.go b/node/node.go index 47ab9a6df4..c0e73a89f9 100644 --- a/node/node.go +++ b/node/node.go @@ -267,7 +267,7 @@ func MakeFull(log logging.Logger, rootDir string, cfg config.Local, phonebookAdd return nil, err } if catchpointCatchupState != ledger.CatchpointCatchupStateInactive { - node.catchpointCatchupService, err = catchup.MakeResumedCatchpointCatchupService(context.Background(), node, node.log, node.net, node.ledger.Ledger) + node.catchpointCatchupService, err = catchup.MakeResumedCatchpointCatchupService(context.Background(), node, node.log, node.net, node.ledger.Ledger, node.config) if err != nil { log.Errorf("unable to create catchpoint catchup service: %v", err) return nil, err @@ -851,7 +851,7 @@ func (node *AlgorandFullNode) StartCatchup(catchpoint string) error { return fmt.Errorf("unable to start catchpoint catchup for '%s' - already catching up '%s'", catchpoint, stats.CatchpointLabel) } var err error - node.catchpointCatchupService, err = catchup.MakeNewCatchpointCatchupService(catchpoint, node, node.log, node.net, node.ledger.Ledger) + node.catchpointCatchupService, err = catchup.MakeNewCatchpointCatchupService(catchpoint, node, node.log, node.net, node.ledger.Ledger, node.config) if err != nil { node.log.Warnf("unable to create catchpoint catchup service : %v", err) return err diff --git a/test/testdata/configs/config-v9.json b/test/testdata/configs/config-v9.json new file mode 100644 index 0000000000..376a38ef0a --- /dev/null +++ b/test/testdata/configs/config-v9.json @@ -0,0 +1,58 @@ +{ + "Version": 9, + "AnnounceParticipationKey": true, + "Archival": false, + "BaseLoggerDebugLevel": 4, + "BroadcastConnectionsLimit": -1, + "CadaverSizeTarget": 1073741824, + "CatchupFailurePeerRefreshRate": 10, + "CatchupParallelBlocks": 16, + "CatchpointInterval": 10000, + "CatchpointFileHistoryLength": 365, + "ConnectionsRateLimitingWindowSeconds": 1, + "ConnectionsRateLimitingCount": 60, + "DeadlockDetection": 0, + "DNSBootstrapID": ".algorand.network", + "DNSSecurityFlags": 1, + "EnableAgreementReporting": false, + "EnableGossipBlockService": true, + "EnableIncomingMessageFilter": false, + "EnableMetricReporting": false, + "EnableOutgoingNetworkMessageFiltering": true, + "EnablePingHandler": true, + "EnableRequestLogger": false, + "EnableTopAccountsReporting": false, + "EndpointAddress": "127.0.0.1:0", + "GossipFanout": 4, + "IncomingConnectionsLimit": 10000, + "IncomingMessageFilterBucketCount": 5, + "IncomingMessageFilterBucketSize": 512, + "LogArchiveMaxAge": "", + "LogArchiveName": "node.archive.log", + "LogSizeLimit": 1073741824, + "MaxConnectionsPerIP": 30, + "NetAddress": "", + "NodeExporterListenAddress": ":9100", + "NodeExporterPath": "./node_exporter", + "OutgoingMessageFilterBucketCount": 3, + "OutgoingMessageFilterBucketSize": 128, + "PriorityPeers": {}, + "ReconnectTime": 60000000000, + "ReservedFDs": 256, + "RestReadTimeoutSeconds": 15, + "RestWriteTimeoutSeconds": 120, + "RunHosted": false, + "SuggestedFeeBlockHistory": 3, + "TelemetryToLog": true, + "TxPoolExponentialIncreaseFactor": 2, + "TxPoolSize": 15000, + "TxSyncIntervalSeconds": 60, + "TxSyncTimeoutSeconds": 30, + "TxSyncServeResponseSize": 1000000, + "SuggestedFeeSlidingWindowSize": 50, + "PeerConnectionsUpdateInterval": 3600, + "CatchupHTTPBlockFetchTimeoutSec": 4, + "CatchupGossipBlockFetchTimeoutSec": 4, + "CatchupBlockDownloadRetryAttempts": 1000, + "CatchupLedgerDownloadRetryAttempts": 50 +} From 3a9bca4799a60bf1d1c99a5e8ad6f243ec544610 Mon Sep 17 00:00:00 2001 From: algomaxj <65551122+algomaxj@users.noreply.github.com> Date: Thu, 11 Jun 2020 15:56:38 -0400 Subject: [PATCH 046/267] Start merging Applications types (#1138) This PR merges some of the the non-ledger pieces of Applications, crossing into the ledger package only to make minor changes to fix the build. --- Makefile | 4 +- cmd/opdoc/opdoc.go | 98 +- cmd/opdoc/tmLanguage.go | 216 + config/consensus.go | 163 +- .../algod/api/server/v1/handlers/handlers.go | 2 +- data/basics/msgp_gen.go | 5537 +++++++++++++---- data/basics/msgp_gen_test.go | 496 ++ data/basics/overflow.go | 18 + data/basics/sort.go | 44 + data/basics/teal.go | 315 + data/basics/teal_test.go | 112 + data/basics/userBalance.go | 141 +- data/basics/userBalance_test.go | 17 +- data/transactions/application.go | 791 +++ data/transactions/application_string.go | 28 + data/transactions/application_test.go | 1404 +++++ data/transactions/keyreg_test.go | 22 +- data/transactions/logic/Makefile | 7 +- data/transactions/logic/README.md | 104 +- data/transactions/logic/README_in.md | 28 +- data/transactions/logic/TEAL_opcodes.md | 359 +- data/transactions/logic/assembler.go | 1104 ++-- data/transactions/logic/assembler_test.go | 1180 +++- .../transactions/logic/backwardCompat_test.go | 454 ++ data/transactions/logic/debugger.go | 281 + data/transactions/logic/debugger_test.go | 127 + data/transactions/logic/doc.go | 216 +- data/transactions/logic/doc_test.go | 33 + data/transactions/logic/eval.go | 1320 +++- data/transactions/logic/evalStateful_test.go | 2647 ++++++++ data/transactions/logic/eval_test.go | 3095 ++++++--- data/transactions/logic/fields.go | 426 ++ data/transactions/logic/fields_string.go | 150 + data/transactions/logic/globalfield_string.go | 28 - data/transactions/logic/kvcow.go | 73 + data/transactions/logic/opcodes.go | 259 + data/transactions/logic/opcodes_test.go | 152 + data/transactions/logic/txnfield_string.go | 47 - data/transactions/msgp_gen.go | 1471 ++++- data/transactions/msgp_gen_test.go | 62 + data/transactions/payment.go | 10 + data/transactions/payment_test.go | 14 +- data/transactions/transaction.go | 122 +- ledger/accountdb.go | 6 +- ledger/acctupdates.go | 12 +- ledger/eval.go | 14 +- ledger/ledger.go | 2 +- logging/logspec/agreementtype_string.go | 33 + logging/logspec/component_string.go | 12 + logging/logspec/ledgertype_string.go | 8 + protocol/codec.go | 5 + protocol/txntype.go | 3 + 52 files changed, 20049 insertions(+), 3223 deletions(-) create mode 100644 cmd/opdoc/tmLanguage.go create mode 100644 data/basics/teal.go create mode 100644 data/basics/teal_test.go create mode 100644 data/transactions/application.go create mode 100644 data/transactions/application_string.go create mode 100644 data/transactions/application_test.go create mode 100644 data/transactions/logic/backwardCompat_test.go create mode 100644 data/transactions/logic/debugger.go create mode 100644 data/transactions/logic/debugger_test.go create mode 100644 data/transactions/logic/evalStateful_test.go create mode 100644 data/transactions/logic/fields.go create mode 100644 data/transactions/logic/fields_string.go delete mode 100644 data/transactions/logic/globalfield_string.go create mode 100644 data/transactions/logic/kvcow.go create mode 100644 data/transactions/logic/opcodes.go create mode 100644 data/transactions/logic/opcodes_test.go delete mode 100644 data/transactions/logic/txnfield_string.go diff --git a/Makefile b/Makefile index 74152271dc..68e1afd239 100644 --- a/Makefile +++ b/Makefile @@ -190,13 +190,13 @@ test: build fulltest: build-race for PACKAGE_DIRECTORY in $(UNIT_TEST_SOURCES) ; do \ - go test $(GOTAGS) -timeout 2000s -race $$PACKAGE_DIRECTORY; \ + go test $(GOTAGS) -timeout 2500s -race $$PACKAGE_DIRECTORY; \ done shorttest: build-race $(addprefix short_test_target_, $(UNIT_TEST_SOURCES)) $(addprefix short_test_target_, $(UNIT_TEST_SOURCES)): build - @go test $(GOTAGS) -short -timeout 2000s -race $(subst short_test_target_,,$@) + @go test $(GOTAGS) -short -timeout 2500s -race $(subst short_test_target_,,$@) integration: build-race ./test/scripts/run_integration_tests.sh diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go index d2ebcd9277..032fa76b08 100644 --- a/cmd/opdoc/opdoc.go +++ b/cmd/opdoc/opdoc.go @@ -51,6 +51,24 @@ func typeEnumTableMarkdown(out io.Writer) { out.Write([]byte("\n")) } +func integerConstantsTableMarkdown(out io.Writer) { + fmt.Fprintf(out, "#### OnComplete\n") + fmt.Fprintf(out, "| Value | Constant name | Description |\n") + fmt.Fprintf(out, "| --- | --- | --- |\n") + for i, name := range logic.OnCompletionNames { + value := uint64(i) + fmt.Fprintf(out, "| %d | %s | %s |\n", value, markdownTableEscape(name), logic.OnCompletionDescription(value)) + } + fmt.Fprintf(out, "\n") + fmt.Fprintf(out, "#### TypeEnum constants\n") + fmt.Fprintf(out, "| Value | Constant name | Description |\n") + fmt.Fprintf(out, "| --- | --- | --- |\n") + for i, name := range logic.TxnTypeNames { + fmt.Fprintf(out, "| %d | %s | %s |\n", i, markdownTableEscape(name), logic.TypeNameDescription(name)) + } + out.Write([]byte("\n")) +} + func fieldTableMarkdown(out io.Writer, names []string, types []logic.StackType, extra map[string]string) { fmt.Fprintf(out, "| Index | Name | Type | Notes |\n") fmt.Fprintf(out, "| --- | --- | --- | --- |\n") @@ -64,19 +82,32 @@ func fieldTableMarkdown(out io.Writer, names []string, types []logic.StackType, func transactionFieldsMarkdown(out io.Writer) { fmt.Fprintf(out, "\n`txn` Fields:\n\n") - fieldTableMarkdown(out, logic.TxnFieldNames, logic.TxnFieldTypes, logic.TxnFieldDocs) + fieldTableMarkdown(out, logic.TxnFieldNames, logic.TxnFieldTypes, logic.TxnFieldDocs()) } func globalFieldsMarkdown(out io.Writer) { fmt.Fprintf(out, "\n`global` Fields:\n\n") - fieldTableMarkdown(out, logic.GlobalFieldNames, logic.GlobalFieldTypes, logic.GlobalFieldDocs) + fieldTableMarkdown(out, logic.GlobalFieldNames, logic.GlobalFieldTypes, logic.GlobalFieldDocs()) } -func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { +func assetHoldingFieldsMarkdown(out io.Writer) { + fmt.Fprintf(out, "\n`asset_holding_get` Fields:\n\n") + fieldTableMarkdown(out, logic.AssetHoldingFieldNames, logic.AssetHoldingFieldTypes, logic.AssetHoldingFieldDocs) +} + +func assetParamsFieldsMarkdown(out io.Writer) { + fmt.Fprintf(out, "\n`asset_params_get` Fields:\n\n") + fieldTableMarkdown(out, logic.AssetParamsFieldNames, logic.AssetParamsFieldTypes, logic.AssetParamsFieldDocs) +} +func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { + ws := "" opextra := logic.OpImmediateNote(op.Name) - cost := logic.OpCost(op.Name) - fmt.Fprintf(out, "\n## %s\n\n- Opcode: 0x%02x %s\n", op.Name, op.Opcode, opextra) + if opextra != "" { + ws = " " + } + costs := logic.OpAllCosts(op.Name) + fmt.Fprintf(out, "\n## %s\n\n- Opcode: 0x%02x%s%s\n", op.Name, op.Opcode, ws, opextra) if op.Args == nil { fmt.Fprintf(out, "- Pops: _None_\n") } else if len(op.Args) == 1 { @@ -98,8 +129,23 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { fmt.Fprintf(out, "\n") } fmt.Fprintf(out, "- %s\n", logic.OpDoc(op.Name)) - if cost != 1 { - fmt.Fprintf(out, "- **Cost**: %d\n", cost) + // if cost changed with versions print all of them + if len(costs) > 1 { + fmt.Fprintf(out, "- **Cost**:\n") + for v := 1; v < len(costs); v++ { + fmt.Fprintf(out, " - %d (LogicSigVersion = %d)\n", costs[v], v) + } + } else { + cost := costs[0] + if cost != 1 { + fmt.Fprintf(out, "- **Cost**: %d\n", cost) + } + } + if op.Version > 1 { + fmt.Fprintf(out, "- LogicSigVersion >= %d\n", op.Version) + } + if !op.Modes.Any() { + fmt.Fprintf(out, "- Mode: %s\n", op.Modes.String()) } if op.Name == "global" { globalFieldsMarkdown(out) @@ -107,6 +153,10 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { transactionFieldsMarkdown(out) fmt.Fprintf(out, "\nTypeEnum mapping:\n\n") typeEnumTableMarkdown(out) + } else if op.Name == "asset_holding_get" { + assetHoldingFieldsMarkdown(out) + } else if op.Name == "asset_params_get" { + assetParamsFieldsMarkdown(out) } ode := logic.OpDocExtra(op.Name) if ode != "" { @@ -117,8 +167,9 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec) (err error) { func opsToMarkdown(out io.Writer) (err error) { out.Write([]byte("# Opcodes\n\nOps have a 'cost' of 1 unless otherwise specified.\n\n")) - for i := range logic.OpSpecs { - err = opToMarkdown(out, &logic.OpSpecs[i]) + opSpecs := logic.OpcodesByVersion(logic.LogicVersion) + for _, spec := range opSpecs { + err = opToMarkdown(out, &spec) if err != nil { return } @@ -194,8 +245,9 @@ func argEnumTypes(name string) string { } func buildLanguageSpec(opGroups map[string][]string) *LanguageSpec { - records := make([]OpRecord, len(logic.OpSpecs)) - for i, spec := range logic.OpSpecs { + opSpecs := logic.OpcodesByVersion(logic.LogicVersion) + records := make([]OpRecord, len(opSpecs)) + for i, spec := range opSpecs { records[i].Opcode = spec.Opcode records[i].Name = spec.Name records[i].Args = typeString(spec.Args) @@ -210,7 +262,7 @@ func buildLanguageSpec(opGroups map[string][]string) *LanguageSpec { records[i].Groups = opGroups[spec.Name] } return &LanguageSpec{ - EvalMaxVersion: logic.EvalMaxVersion, + EvalMaxVersion: logic.LogicVersion, LogicSigVersion: config.Consensus[protocol.ConsensusCurrentVersion].LogicSigVersion, Ops: records, } @@ -231,16 +283,34 @@ func main() { opGroups[opname] = append(opGroups[opname], og.GroupName) } } + constants, _ := os.Create("named_integer_constants.md") + integerConstantsTableMarkdown(constants) + constants.Close() + txnfields, _ := os.Create("txn_fields.md") - fieldTableMarkdown(txnfields, logic.TxnFieldNames, logic.TxnFieldTypes, logic.TxnFieldDocs) + fieldTableMarkdown(txnfields, logic.TxnFieldNames, logic.TxnFieldTypes, logic.TxnFieldDocs()) txnfields.Close() globalfields, _ := os.Create("global_fields.md") - fieldTableMarkdown(globalfields, logic.GlobalFieldNames, logic.GlobalFieldTypes, logic.GlobalFieldDocs) + fieldTableMarkdown(globalfields, logic.GlobalFieldNames, logic.GlobalFieldTypes, logic.GlobalFieldDocs()) globalfields.Close() + assetholding, _ := os.Create("asset_holding_fields.md") + fieldTableMarkdown(assetholding, logic.AssetHoldingFieldNames, logic.AssetHoldingFieldTypes, logic.AssetHoldingFieldDocs) + assetholding.Close() + + assetparams, _ := os.Create("asset_params_fields.md") + fieldTableMarkdown(assetparams, logic.AssetParamsFieldNames, logic.AssetParamsFieldTypes, logic.AssetParamsFieldDocs) + assetparams.Close() + langspecjs, _ := os.Create("langspec.json") enc := json.NewEncoder(langspecjs) enc.Encode(buildLanguageSpec(opGroups)) langspecjs.Close() + + tealtm, _ := os.Create("teal.tmLanguage.json") + enc = json.NewEncoder(tealtm) + enc.SetIndent("", " ") + enc.Encode(buildSyntaxHighlight()) + tealtm.Close() } diff --git a/cmd/opdoc/tmLanguage.go b/cmd/opdoc/tmLanguage.go new file mode 100644 index 0000000000..2a96a1d636 --- /dev/null +++ b/cmd/opdoc/tmLanguage.go @@ -0,0 +1,216 @@ +// 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 . + +package main + +import ( + "fmt" + "strings" + + "github.com/algorand/go-algorand/data/transactions/logic" +) + +// TEAL syntax highlighter as TM grammar. +// See the following resources for more info: +// +// 1. tmLanguage grammar +// https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json +// 2. tmLanguage description +// https://macromates.com/manual/en/language_grammars +// 3. Oniguruma regular expressions +// https://macromates.com/manual/en/regular_expressions + +type tmLanguage struct { + Schema string `json:"$schema,omitempty"` + Name string `json:"name"` + Patterns []pattern `json:"patterns,omitempty"` + Repository map[string]pattern `json:"repository,omitempty"` + ScopeName string `json:"scopeName"` +} + +type pattern struct { + Include string `json:"include,omitempty"` + Name string `json:"name,omitempty"` + Begin string `json:"begin,omitempty"` + End string `json:"end,omitempty"` + Match string `json:"match,omitempty"` + Captures map[string]pattern `json:"captures,omitempty"` + Patterns []pattern `json:"patterns,omitempty"` +} + +func buildSyntaxHighlight() *tmLanguage { + tm := tmLanguage{ + Schema: "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + Name: "Algorand TEAL", + ScopeName: "source.teal", + Patterns: []pattern{ + {Include: "#invalid"}, + {Include: "#comments"}, + {Include: "#strings"}, + {Include: "#literals"}, + {Include: "#labels"}, + {Include: "#keywords"}, + {Include: "#pragmas"}, + }, + Repository: map[string]pattern{}, + } + tm.Repository["invalid"] = pattern{ + Patterns: []pattern{{ + Name: "invalid.illegal.teal", + Match: "^\\s+.*$", + }}, + } + tm.Repository["comments"] = pattern{ + Name: "comment.line.double-slash.teal", + Begin: "//", + End: "$", + } + tm.Repository["strings"] = pattern{ + Name: "string.quoted.double.teal", + Begin: "\"", + End: "\"", + Patterns: []pattern{{ + Name: "constant.character.escape.teal", + Match: "\\\\(x[0-9A-Fa-f]{2}|.|$)", + }}, + } + tm.Repository["pragmas"] = pattern{ + Name: "support.function.teal", + Match: "^#pragma\\b.*$", + } + tm.Repository["labels"] = pattern{ + Patterns: []pattern{ + { + Name: "support.variable.teal", + Match: "^\\w+:.*$", + }, + { + Match: "\\b(?<=b|bz|bnz)\\s+(\\w+)\\b", + Captures: map[string]pattern{ + "1": {Name: "support.variable.teal"}, + }, + }, + }, + } + literals := pattern{ + Patterns: []pattern{ + { + Name: "constant.numeric.teal", + Match: "\\b([0-9]+)\\b", + }, + { + Name: "constant.numeric.teal", + Match: "\\b(?<=int\\s+)(0x[0-9]+)\\b", + }, + { + Name: "string.quoted.double.teal", + Match: "\\b(?<=byte\\s+)(0x[0-9]+)\\b", + }, + }, + } + var allNamedFields []string + allNamedFields = append(allNamedFields, logic.TxnFieldNames...) + allNamedFields = append(allNamedFields, logic.GlobalFieldNames...) + allNamedFields = append(allNamedFields, logic.AssetHoldingFieldNames...) + allNamedFields = append(allNamedFields, logic.AssetParamsFieldNames...) + allNamedFields = append(allNamedFields, logic.OnCompletionNames...) + + literals.Patterns = append(literals.Patterns, pattern{ + Name: "variable.parameter.teal", + Match: fmt.Sprintf("\\b(%s)\\b", strings.Join(allNamedFields, "|")), + }) + tm.Repository["literals"] = literals + + keywords := pattern{ + Patterns: []pattern{ + { + Match: "\\b(base64|b64|base32|b32)(?:\\(|\\s+)([a-zA-Z0-9\\+\\/\\=]+)(?:\\)|\\s?|$)", + Captures: map[string]pattern{ + "1": {Name: "support.class.teal"}, + "2": {Name: "string.quoted.triple.teal"}, + }, + }, + { + Match: "^(addr)\\s+([A-Z2-7\\=]+)", + Captures: map[string]pattern{ + "1": {Name: "keyword.other.teal"}, + "2": {Name: "string.unquoted.teal"}, + }, + }, + }, + } + for _, opgroup := range logic.OpGroupList { + switch opgroup.GroupName { + case "Flow Control": + keywords.Patterns = append(keywords.Patterns, pattern{ + Name: "keyword.control.teal", + Match: fmt.Sprintf("^(%s)\\b", strings.Join(opgroup.Ops, "|")), + }) + case "Loading Values": + loading := []string{"int", "byte", "addr"} + loading = append(loading, opgroup.Ops...) + keywords.Patterns = append(keywords.Patterns, pattern{ + Name: "keyword.other.teal", + Match: fmt.Sprintf("^(%s)\\b", strings.Join(loading, "|")), + }) + case "State Access": + keywords.Patterns = append(keywords.Patterns, pattern{ + Name: "keyword.other.unit.teal", + Match: fmt.Sprintf("^(%s)\\b", strings.Join(opgroup.Ops, "|")), + }) + case "Arithmetic": + escape := map[rune]bool{ + '*': true, + '+': true, + '=': true, + '-': true, + '!': true, + '~': true, + '^': true, + '$': true, + '?': true, + '|': true, + '<': true, + '>': true, + } + var allArithmetics []string + for _, op := range opgroup.Ops { + if len(op) < 3 { + // all symbol-based opcodes are under 3 chars, and no trigraphs so far + escaped := make([]byte, 0, len(op)*2) + for _, ch := range op { + if _, ok := escape[ch]; ok { + escaped = append(escaped, '\\') + } + escaped = append(escaped, byte(ch)) + } + allArithmetics = append(allArithmetics, string(escaped)) + } else { + allArithmetics = append(allArithmetics, op) + } + } + keywords.Patterns = append(keywords.Patterns, pattern{ + Name: "keyword.operator.teal", + Match: fmt.Sprintf("^(%s)\\b", strings.Join(allArithmetics, "|")), + }) + default: + panic(fmt.Sprintf("Unknown ops group: %s", opgroup.GroupName)) + } + } + tm.Repository["keywords"] = keywords + + return &tm +} diff --git a/config/consensus.go b/config/consensus.go index e9938ca9b0..6942bc208c 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -217,6 +217,80 @@ type ConsensusParams struct { // SupportRekeying indicates support for account rekeying (the RekeyTo and AuthAddr fields) SupportRekeying bool + + // application support + Application bool + + // max number of ApplicationArgs for an ApplicationCall transaction + MaxAppArgs int + + // max sum([len(arg) for arg in txn.ApplicationArgs]) + MaxAppTotalArgLen int + + // maximum length of application approval program or clear state + // program in bytes + MaxAppProgramLen int + + // maximum number of accounts in the ApplicationCall Accounts field. + // this determines, in part, the maximum number of balance records + // accessed by a single transaction + MaxAppTxnAccounts int + + // maximum number of app ids in the ApplicationCall ForeignApps field. + // these are the only applications besides the called application for + // which global state may be read in the transaction + MaxAppTxnForeignApps int + + // maximum cost of application approval program or clear state program + MaxAppProgramCost int + + // maximum length of a key used in an application's global or local + // key/value store + MaxAppKeyLen int + + // maximum length of a bytes value used in an application's global or + // local key/value store + MaxAppBytesValueLen int + + // maximum number of applications a single account can create and store + // AppParams for at once + MaxAppsCreated int + + // maximum number of applications a single account can opt in to and + // store AppLocalState for at once + MaxAppsOptedIn int + + // flat MinBalance requirement for creating a single application and + // storing its AppParams + AppFlatParamsMinBalance uint64 + + // flat MinBalance requirement for opting in to a single application + // and storing its AppLocalState + AppFlatOptInMinBalance uint64 + + // MinBalance requirement per key/value entry in LocalState or + // GlobalState key/value stores, regardless of value type + SchemaMinBalancePerEntry uint64 + + // MinBalance requirement (in addition to SchemaMinBalancePerEntry) for + // integer values stored in LocalState or GlobalState key/value stores + SchemaUintMinBalance uint64 + + // MinBalance requirement (in addition to SchemaMinBalancePerEntry) for + // []byte values stored in LocalState or GlobalState key/value stores + SchemaBytesMinBalance uint64 + + // maximum number of total key/value pairs allowed by a given + // LocalStateSchema (and therefore allowed in LocalState) + MaxLocalSchemaEntries uint64 + + // maximum number of total key/value pairs allowed by a given + // GlobalStateSchema (and therefore allowed in GlobalState) + MaxGlobalSchemaEntries uint64 + + // maximum total minimum balance requirement for an account, used + // to limit the maximum size of a single balance record + MaximumMinimumBalance uint64 } // ConsensusProtocols defines a set of supported protocol versions and their @@ -231,12 +305,38 @@ var Consensus ConsensusProtocols // consensus protocols, used for decoding purposes. var MaxVoteThreshold int -func maybeMaxVoteThreshold(t uint64) { - if int(t) > MaxVoteThreshold { - MaxVoteThreshold = int(t) +// MaxEvalDeltaAccounts is the largest number of accounts that may appear in +// an eval delta, used for decoding purposes. +var MaxEvalDeltaAccounts int + +// MaxStateDeltaKeys is the largest number of key/value pairs that may appear +// in a StateDelta, used for decoding purposes. +var MaxStateDeltaKeys int + +func checkSetMax(value int, curMax *int) { + if value > *curMax { + *curMax = value } } +// checkSetAllocBounds sets some global variables used during msgpack decoding +// to enforce memory allocation limits. The values should be generous to +// prevent correctness bugs, but not so large that DoS attacks are trivial +func checkSetAllocBounds(p ConsensusParams) { + checkSetMax(int(p.SoftCommitteeThreshold), &MaxVoteThreshold) + checkSetMax(int(p.CertCommitteeThreshold), &MaxVoteThreshold) + checkSetMax(int(p.NextCommitteeThreshold), &MaxVoteThreshold) + checkSetMax(int(p.LateCommitteeThreshold), &MaxVoteThreshold) + checkSetMax(int(p.RedoCommitteeThreshold), &MaxVoteThreshold) + checkSetMax(int(p.DownCommitteeThreshold), &MaxVoteThreshold) + + // These bounds could be tighter, but since these values are just to + // prevent DoS, setting them to be the maximum number of allowed + // executed TEAL instructions should be fine (order of ~1000) + checkSetMax(p.MaxAppProgramLen, &MaxStateDeltaKeys) + checkSetMax(p.MaxAppProgramLen, &MaxEvalDeltaAccounts) +} + // SaveConfigurableConsensus saves the configurable protocols file to the provided data directory. func SaveConfigurableConsensus(dataDirectory string, params ConsensusProtocols) error { consensusProtocolPath := filepath.Join(dataDirectory, ConfigurableConsensusProtocolsFilename) @@ -572,7 +672,56 @@ func initConsensusProtocols() { // but not yet released in a production protocol version. vFuture := v23 vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + vFuture.LogicSigVersion = 2 + + // Enable application support + vFuture.Application = true + + // Enable rekeying vFuture.SupportRekeying = true + + // 100.1 Algos (MinBalance for creating 1,000 assets) + vFuture.MaximumMinimumBalance = 100100000 + + vFuture.MaxAppArgs = 16 + vFuture.MaxAppTotalArgLen = 2048 + vFuture.MaxAppProgramLen = 1024 + vFuture.MaxAppKeyLen = 64 + vFuture.MaxAppBytesValueLen = 64 + + // 0.1 Algos (Same min balance cost as an Asset) + vFuture.AppFlatParamsMinBalance = 100000 + vFuture.AppFlatOptInMinBalance = 100000 + + // Can look up Sender + 4 other balance records per Application txn + vFuture.MaxAppTxnAccounts = 4 + + // Can look up 2 other app creator balance records to see global state + vFuture.MaxAppTxnForeignApps = 2 + + // 64 byte keys @ ~333 microAlgos/byte + delta + vFuture.SchemaMinBalancePerEntry = 25000 + + // 9 bytes @ ~333 microAlgos/byte + delta + vFuture.SchemaUintMinBalance = 3500 + + // 64 byte values @ ~333 microAlgos/byte + delta + vFuture.SchemaBytesMinBalance = 25000 + + // Maximum number of key/value pairs per local key/value store + vFuture.MaxLocalSchemaEntries = 16 + + // Maximum number of key/value pairs per global key/value store + vFuture.MaxGlobalSchemaEntries = 64 + + // Maximum cost of ApprovalProgram/ClearStateProgram + vFuture.MaxAppProgramCost = 700 + + // Maximum number of apps a single account can create + vFuture.MaxAppsCreated = 10 + + // Maximum number of apps a single account can opt into + vFuture.MaxAppsOptedIn = 10 Consensus[protocol.ConsensusFuture] = vFuture } @@ -603,12 +752,8 @@ func init() { Protocol.SmallLambda = time.Duration(algoSmallLambda) * time.Millisecond } + // Set allocation limits for _, p := range Consensus { - maybeMaxVoteThreshold(p.SoftCommitteeThreshold) - maybeMaxVoteThreshold(p.CertCommitteeThreshold) - maybeMaxVoteThreshold(p.NextCommitteeThreshold) - maybeMaxVoteThreshold(p.LateCommitteeThreshold) - maybeMaxVoteThreshold(p.RedoCommitteeThreshold) - maybeMaxVoteThreshold(p.DownCommitteeThreshold) + checkSetAllocBounds(p) } } diff --git a/daemon/algod/api/server/v1/handlers/handlers.go b/daemon/algod/api/server/v1/handlers/handlers.go index e34d1d98ae..d1f12b407b 100644 --- a/daemon/algod/api/server/v1/handlers/handlers.go +++ b/daemon/algod/api/server/v1/handlers/handlers.go @@ -1216,7 +1216,7 @@ func Assets(ctx lib.ReqContext, context echo.Context) { } // Ensure no race with asset deletion - rp, ok := creatorRecord.AssetParams[aloc.Index] + rp, ok := creatorRecord.AssetParams[basics.AssetIndex(aloc.Index)] if !ok { continue } diff --git a/data/basics/msgp_gen.go b/data/basics/msgp_gen.go index 479bf8e329..a088426460 100644 --- a/data/basics/msgp_gen.go +++ b/data/basics/msgp_gen.go @@ -5,6 +5,7 @@ package basics import ( "sort" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/msgp/msgp" ) @@ -26,6 +27,30 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // +// AppIndex +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// AppLocalState +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// AppParams +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // AssetHolding // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -58,6 +83,38 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // +// CreatableIndex +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// CreatableType +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// DeltaAction +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// EvalDelta +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // Round // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -74,6 +131,22 @@ import ( // |-----> Msgsize // |-----> MsgIsZero // +// StateDelta +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// StateSchema +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // Status // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -82,65 +155,109 @@ import ( // |-----> Msgsize // |-----> MsgIsZero // +// TealKeyValue +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// TealType +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// +// TealValue +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// +// ValueDelta +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // MarshalMsg implements msgp.Marshaler func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0005Len := uint32(12) - var zb0005Mask uint16 /* 13 bits */ + zb0009Len := uint32(15) + var zb0009Mask uint16 /* 16 bits */ if (*z).MicroAlgos.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x2 + zb0009Len-- + zb0009Mask |= 0x2 } if len((*z).AssetParams) == 0 { - zb0005Len-- - zb0005Mask |= 0x4 + zb0009Len-- + zb0009Mask |= 0x4 + } + if len((*z).AppLocalStates) == 0 { + zb0009Len-- + zb0009Mask |= 0x8 + } + if len((*z).AppParams) == 0 { + zb0009Len-- + zb0009Mask |= 0x10 } if len((*z).Assets) == 0 { - zb0005Len-- - zb0005Mask |= 0x8 + zb0009Len-- + zb0009Mask |= 0x20 } if (*z).RewardsBase == 0 { - zb0005Len-- - zb0005Mask |= 0x10 + zb0009Len-- + zb0009Mask |= 0x40 } if (*z).RewardedMicroAlgos.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x20 + zb0009Len-- + zb0009Mask |= 0x80 } if (*z).Status == 0 { - zb0005Len-- - zb0005Mask |= 0x40 + zb0009Len-- + zb0009Mask |= 0x100 } if (*z).SelectionID.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x80 + zb0009Len-- + zb0009Mask |= 0x200 } if (*z).AuthAddr.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x100 + zb0009Len-- + zb0009Mask |= 0x400 + } + if ((*z).TotalAppSchema.NumUint == 0) && ((*z).TotalAppSchema.NumByteSlice == 0) { + zb0009Len-- + zb0009Mask |= 0x800 } if (*z).VoteID.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x200 + zb0009Len-- + zb0009Mask |= 0x1000 } if (*z).VoteFirstValid == 0 { - zb0005Len-- - zb0005Mask |= 0x400 + zb0009Len-- + zb0009Mask |= 0x2000 } if (*z).VoteKeyDilution == 0 { - zb0005Len-- - zb0005Mask |= 0x800 + zb0009Len-- + zb0009Mask |= 0x4000 } if (*z).VoteLastValid == 0 { - zb0005Len-- - zb0005Mask |= 0x1000 + zb0009Len-- + zb0009Mask |= 0x8000 } - // variable map header, size zb0005Len - o = append(o, 0x80|uint8(zb0005Len)) - if zb0005Len != 0 { - if (zb0005Mask & 0x2) == 0 { // if not empty + // variable map header, size zb0009Len + o = append(o, 0x80|uint8(zb0009Len)) + if zb0009Len != 0 { + if (zb0009Mask & 0x2) == 0 { // if not empty // string "algo" o = append(o, 0xa4, 0x61, 0x6c, 0x67, 0x6f) o, err = (*z).MicroAlgos.MarshalMsg(o) @@ -149,7 +266,7 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x4) == 0 { // if not empty + if (zb0009Mask & 0x4) == 0 { // if not empty // string "apar" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x72) if (*z).AssetParams == nil { @@ -177,7 +294,63 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0005Mask & 0x8) == 0 { // if not empty + if (zb0009Mask & 0x8) == 0 { // if not empty + // string "appl" + o = append(o, 0xa4, 0x61, 0x70, 0x70, 0x6c) + if (*z).AppLocalStates == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AppLocalStates))) + } + zb0005_keys := make([]AppIndex, 0, len((*z).AppLocalStates)) + for zb0005 := range (*z).AppLocalStates { + zb0005_keys = append(zb0005_keys, zb0005) + } + sort.Sort(SortAppIndex(zb0005_keys)) + for _, zb0005 := range zb0005_keys { + zb0006 := (*z).AppLocalStates[zb0005] + _ = zb0006 + o, err = zb0005.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + o, err = zb0006.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + } + } + if (zb0009Mask & 0x10) == 0 { // if not empty + // string "appp" + o = append(o, 0xa4, 0x61, 0x70, 0x70, 0x70) + if (*z).AppParams == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AppParams))) + } + zb0007_keys := make([]AppIndex, 0, len((*z).AppParams)) + for zb0007 := range (*z).AppParams { + zb0007_keys = append(zb0007_keys, zb0007) + } + sort.Sort(SortAppIndex(zb0007_keys)) + for _, zb0007 := range zb0007_keys { + zb0008 := (*z).AppParams[zb0007] + _ = zb0008 + o, err = zb0007.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + o, err = zb0008.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + } + } + if (zb0009Mask & 0x20) == 0 { // if not empty // string "asset" o = append(o, 0xa5, 0x61, 0x73, 0x73, 0x65, 0x74) if (*z).Assets == nil { @@ -199,25 +372,25 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } // omitempty: check for empty values - zb0006Len := uint32(2) - var zb0006Mask uint8 /* 3 bits */ + zb0010Len := uint32(2) + var zb0010Mask uint8 /* 3 bits */ if zb0004.Amount == 0 { - zb0006Len-- - zb0006Mask |= 0x2 + zb0010Len-- + zb0010Mask |= 0x2 } if zb0004.Frozen == false { - zb0006Len-- - zb0006Mask |= 0x4 + zb0010Len-- + zb0010Mask |= 0x4 } - // variable map header, size zb0006Len - o = append(o, 0x80|uint8(zb0006Len)) - if zb0006Len != 0 { - if (zb0006Mask & 0x2) == 0 { // if not empty + // variable map header, size zb0010Len + o = append(o, 0x80|uint8(zb0010Len)) + if zb0010Len != 0 { + if (zb0010Mask & 0x2) == 0 { // if not empty // string "a" o = append(o, 0xa1, 0x61) o = msgp.AppendUint64(o, zb0004.Amount) } - if (zb0006Mask & 0x4) == 0 { // if not empty + if (zb0010Mask & 0x4) == 0 { // if not empty // string "f" o = append(o, 0xa1, 0x66) o = msgp.AppendBool(o, zb0004.Frozen) @@ -225,12 +398,12 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0005Mask & 0x10) == 0 { // if not empty + if (zb0009Mask & 0x40) == 0 { // if not empty // string "ebase" o = append(o, 0xa5, 0x65, 0x62, 0x61, 0x73, 0x65) o = msgp.AppendUint64(o, (*z).RewardsBase) } - if (zb0005Mask & 0x20) == 0 { // if not empty + if (zb0009Mask & 0x80) == 0 { // if not empty // string "ern" o = append(o, 0xa3, 0x65, 0x72, 0x6e) o, err = (*z).RewardedMicroAlgos.MarshalMsg(o) @@ -239,12 +412,12 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x40) == 0 { // if not empty + if (zb0009Mask & 0x100) == 0 { // if not empty // string "onl" o = append(o, 0xa3, 0x6f, 0x6e, 0x6c) o = msgp.AppendByte(o, byte((*z).Status)) } - if (zb0005Mask & 0x80) == 0 { // if not empty + if (zb0009Mask & 0x200) == 0 { // if not empty // string "sel" o = append(o, 0xa3, 0x73, 0x65, 0x6c) o, err = (*z).SelectionID.MarshalMsg(o) @@ -253,7 +426,7 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x100) == 0 { // if not empty + if (zb0009Mask & 0x400) == 0 { // if not empty // string "spend" o = append(o, 0xa5, 0x73, 0x70, 0x65, 0x6e, 0x64) o, err = (*z).AuthAddr.MarshalMsg(o) @@ -262,7 +435,34 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x200) == 0 { // if not empty + if (zb0009Mask & 0x800) == 0 { // if not empty + // string "tsch" + o = append(o, 0xa4, 0x74, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0011Len := uint32(2) + var zb0011Mask uint8 /* 3 bits */ + if (*z).TotalAppSchema.NumByteSlice == 0 { + zb0011Len-- + zb0011Mask |= 0x2 + } + if (*z).TotalAppSchema.NumUint == 0 { + zb0011Len-- + zb0011Mask |= 0x4 + } + // variable map header, size zb0011Len + o = append(o, 0x80|uint8(zb0011Len)) + if (zb0011Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).TotalAppSchema.NumByteSlice) + } + if (zb0011Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).TotalAppSchema.NumUint) + } + } + if (zb0009Mask & 0x1000) == 0 { // if not empty // string "vote" o = append(o, 0xa4, 0x76, 0x6f, 0x74, 0x65) o, err = (*z).VoteID.MarshalMsg(o) @@ -271,17 +471,17 @@ func (z *AccountData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x400) == 0 { // if not empty + if (zb0009Mask & 0x2000) == 0 { // if not empty // string "voteFst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x46, 0x73, 0x74) o = msgp.AppendUint64(o, uint64((*z).VoteFirstValid)) } - if (zb0005Mask & 0x800) == 0 { // if not empty + if (zb0009Mask & 0x4000) == 0 { // if not empty // string "voteKD" o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x4b, 0x44) o = msgp.AppendUint64(o, (*z).VoteKeyDilution) } - if (zb0005Mask & 0x1000) == 0 { // if not empty + if (zb0009Mask & 0x8000) == 0 { // if not empty // string "voteLst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x4c, 0x73, 0x74) o = msgp.AppendUint64(o, uint64((*z).VoteLastValid)) @@ -299,122 +499,122 @@ func (_ *AccountData) CanMarshalMsg(z interface{}) bool { func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- { - var zb0007 byte - zb0007, bts, err = msgp.ReadByteBytes(bts) + var zb0011 byte + zb0011, bts, err = msgp.ReadByteBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Status") return } - (*z).Status = Status(zb0007) + (*z).Status = Status(zb0011) } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") return } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- (*z).RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "RewardsBase") return } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- bts, err = (*z).RewardedMicroAlgos.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "RewardedMicroAlgos") return } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- bts, err = (*z).VoteID.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteID") return } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- bts, err = (*z).SelectionID.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "SelectionID") return } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- { - var zb0008 uint64 - zb0008, bts, err = msgp.ReadUint64Bytes(bts) + var zb0012 uint64 + zb0012, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") return } - (*z).VoteFirstValid = Round(zb0008) + (*z).VoteFirstValid = Round(zb0012) } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- { - var zb0009 uint64 - zb0009, bts, err = msgp.ReadUint64Bytes(bts) + var zb0013 uint64 + zb0013, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") return } - (*z).VoteLastValid = Round(zb0009) + (*z).VoteLastValid = Round(zb0013) } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") return } } - if zb0005 > 0 { - zb0005-- - var zb0010 int - var zb0011 bool - zb0010, zb0011, bts, err = msgp.ReadMapHeaderBytes(bts) + if zb0009 > 0 { + zb0009-- + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetParams") return } - if zb0010 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxAssetsPerAccount)) + if zb0014 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxAssetsPerAccount)) err = msgp.WrapError(err, "struct-from-array", "AssetParams") return } - if zb0011 { + if zb0015 { (*z).AssetParams = nil } else if (*z).AssetParams == nil { - (*z).AssetParams = make(map[AssetIndex]AssetParams, zb0010) + (*z).AssetParams = make(map[AssetIndex]AssetParams, zb0014) } - for zb0010 > 0 { + for zb0014 > 0 { var zb0001 AssetIndex var zb0002 AssetParams - zb0010-- + zb0014-- bts, err = zb0001.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetParams") @@ -428,61 +628,61 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).AssetParams[zb0001] = zb0002 } } - if zb0005 > 0 { - zb0005-- - var zb0012 int - var zb0013 bool - zb0012, zb0013, bts, err = msgp.ReadMapHeaderBytes(bts) + if zb0009 > 0 { + zb0009-- + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets") return } - if zb0012 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxAssetsPerAccount)) + if zb0016 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxAssetsPerAccount)) err = msgp.WrapError(err, "struct-from-array", "Assets") return } - if zb0013 { + if zb0017 { (*z).Assets = nil } else if (*z).Assets == nil { - (*z).Assets = make(map[AssetIndex]AssetHolding, zb0012) + (*z).Assets = make(map[AssetIndex]AssetHolding, zb0016) } - for zb0012 > 0 { + for zb0016 > 0 { var zb0003 AssetIndex var zb0004 AssetHolding - zb0012-- + zb0016-- bts, err = zb0003.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets") return } - var zb0014 int - var zb0015 bool - zb0014, zb0015, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0018 int + var zb0019 bool + zb0018, zb0019, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) return } - if zb0014 > 0 { - zb0014-- + if zb0018 > 0 { + zb0018-- zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Amount") return } } - if zb0014 > 0 { - zb0014-- + if zb0018 > 0 { + zb0018-- zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Frozen") return } } - if zb0014 > 0 { - err = msgp.ErrTooManyArrayFields(zb0014) + if zb0018 > 0 { + err = msgp.ErrTooManyArrayFields(zb0018) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array") return @@ -493,11 +693,11 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) return } - if zb0015 { + if zb0019 { zb0004 = AssetHolding{} } - for zb0014 > 0 { - zb0014-- + for zb0018 > 0 { + zb0018-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) @@ -528,16 +728,160 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).Assets[zb0003] = zb0004 } } - if zb0005 > 0 { - zb0005-- + if zb0009 > 0 { + zb0009-- bts, err = (*z).AuthAddr.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AuthAddr") return } } - if zb0005 > 0 { - err = msgp.ErrTooManyArrayFields(zb0005) + if zb0009 > 0 { + zb0009-- + var zb0020 int + var zb0021 bool + zb0020, zb0021, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + if zb0020 > encodedMaxAppLocalStates { + err = msgp.ErrOverflow(uint64(zb0020), uint64(encodedMaxAppLocalStates)) + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + if zb0021 { + (*z).AppLocalStates = nil + } else if (*z).AppLocalStates == nil { + (*z).AppLocalStates = make(map[AppIndex]AppLocalState, zb0020) + } + for zb0020 > 0 { + var zb0005 AppIndex + var zb0006 AppLocalState + zb0020-- + bts, err = zb0005.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates", zb0005) + return + } + (*z).AppLocalStates[zb0005] = zb0006 + } + } + if zb0009 > 0 { + zb0009-- + var zb0022 int + var zb0023 bool + zb0022, zb0023, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + if zb0022 > encodedMaxAppParams { + err = msgp.ErrOverflow(uint64(zb0022), uint64(encodedMaxAppParams)) + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + if zb0023 { + (*z).AppParams = nil + } else if (*z).AppParams == nil { + (*z).AppParams = make(map[AppIndex]AppParams, zb0022) + } + for zb0022 > 0 { + var zb0007 AppIndex + var zb0008 AppParams + zb0022-- + bts, err = zb0007.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + bts, err = zb0008.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams", zb0007) + return + } + (*z).AppParams[zb0007] = zb0008 + } + } + if zb0009 > 0 { + zb0009-- + var zb0024 int + var zb0025 bool + zb0024, zb0025, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0024, zb0025, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + if zb0024 > 0 { + zb0024-- + (*z).TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array", "NumUint") + return + } + } + if zb0024 > 0 { + zb0024-- + (*z).TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0024 > 0 { + err = msgp.ErrTooManyArrayFields(zb0024) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + if zb0025 { + (*z).TotalAppSchema = StateSchema{} + } + for zb0024 > 0 { + zb0024-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + switch string(field) { + case "nui": + (*z).TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "NumUint") + return + } + case "nbs": + (*z).TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + } + } + } + } + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -548,11 +892,11 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0006 { + if zb0010 { (*z) = AccountData{} } - for zb0005 > 0 { - zb0005-- + for zb0009 > 0 { + zb0009-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) @@ -561,13 +905,13 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { switch string(field) { case "onl": { - var zb0016 byte - zb0016, bts, err = msgp.ReadByteBytes(bts) + var zb0026 byte + zb0026, bts, err = msgp.ReadByteBytes(bts) if err != nil { err = msgp.WrapError(err, "Status") return } - (*z).Status = Status(zb0016) + (*z).Status = Status(zb0026) } case "algo": bts, err = (*z).MicroAlgos.UnmarshalMsg(bts) @@ -601,23 +945,23 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { } case "voteFst": { - var zb0017 uint64 - zb0017, bts, err = msgp.ReadUint64Bytes(bts) + var zb0027 uint64 + zb0027, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "VoteFirstValid") return } - (*z).VoteFirstValid = Round(zb0017) + (*z).VoteFirstValid = Round(zb0027) } case "voteLst": { - var zb0018 uint64 - zb0018, bts, err = msgp.ReadUint64Bytes(bts) + var zb0028 uint64 + zb0028, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "VoteLastValid") return } - (*z).VoteLastValid = Round(zb0018) + (*z).VoteLastValid = Round(zb0028) } case "voteKD": (*z).VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) @@ -626,27 +970,27 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "apar": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0029 int + var zb0030 bool + zb0029, zb0030, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "AssetParams") return } - if zb0019 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxAssetsPerAccount)) + if zb0029 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0029), uint64(encodedMaxAssetsPerAccount)) err = msgp.WrapError(err, "AssetParams") return } - if zb0020 { + if zb0030 { (*z).AssetParams = nil } else if (*z).AssetParams == nil { - (*z).AssetParams = make(map[AssetIndex]AssetParams, zb0019) + (*z).AssetParams = make(map[AssetIndex]AssetParams, zb0029) } - for zb0019 > 0 { + for zb0029 > 0 { var zb0001 AssetIndex var zb0002 AssetParams - zb0019-- + zb0029-- bts, err = zb0001.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "AssetParams") @@ -660,59 +1004,59 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { (*z).AssetParams[zb0001] = zb0002 } case "asset": - var zb0021 int - var zb0022 bool - zb0021, zb0022, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0031 int + var zb0032 bool + zb0031, zb0032, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Assets") return } - if zb0021 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxAssetsPerAccount)) + if zb0031 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0031), uint64(encodedMaxAssetsPerAccount)) err = msgp.WrapError(err, "Assets") return } - if zb0022 { + if zb0032 { (*z).Assets = nil } else if (*z).Assets == nil { - (*z).Assets = make(map[AssetIndex]AssetHolding, zb0021) + (*z).Assets = make(map[AssetIndex]AssetHolding, zb0031) } - for zb0021 > 0 { + for zb0031 > 0 { var zb0003 AssetIndex var zb0004 AssetHolding - zb0021-- + zb0031-- bts, err = zb0003.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "Assets") return } - var zb0023 int - var zb0024 bool - zb0023, zb0024, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0033 int + var zb0034 bool + zb0033, zb0034, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0023, zb0024, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0033, zb0034, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Assets", zb0003) return } - if zb0023 > 0 { - zb0023-- + if zb0033 > 0 { + zb0033-- zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Amount") return } } - if zb0023 > 0 { - zb0023-- + if zb0033 > 0 { + zb0033-- zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Frozen") return } } - if zb0023 > 0 { - err = msgp.ErrTooManyArrayFields(zb0023) + if zb0033 > 0 { + err = msgp.ErrTooManyArrayFields(zb0033) if err != nil { err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array") return @@ -723,11 +1067,11 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "Assets", zb0003) return } - if zb0024 { + if zb0034 { zb0004 = AssetHolding{} } - for zb0023 > 0 { - zb0023-- + for zb0033 > 0 { + zb0033-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "Assets", zb0003) @@ -763,6 +1107,144 @@ func (z *AccountData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "AuthAddr") return } + case "appl": + var zb0035 int + var zb0036 bool + zb0035, zb0036, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates") + return + } + if zb0035 > encodedMaxAppLocalStates { + err = msgp.ErrOverflow(uint64(zb0035), uint64(encodedMaxAppLocalStates)) + err = msgp.WrapError(err, "AppLocalStates") + return + } + if zb0036 { + (*z).AppLocalStates = nil + } else if (*z).AppLocalStates == nil { + (*z).AppLocalStates = make(map[AppIndex]AppLocalState, zb0035) + } + for zb0035 > 0 { + var zb0005 AppIndex + var zb0006 AppLocalState + zb0035-- + bts, err = zb0005.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates") + return + } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + (*z).AppLocalStates[zb0005] = zb0006 + } + case "appp": + var zb0037 int + var zb0038 bool + zb0037, zb0038, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams") + return + } + if zb0037 > encodedMaxAppParams { + err = msgp.ErrOverflow(uint64(zb0037), uint64(encodedMaxAppParams)) + err = msgp.WrapError(err, "AppParams") + return + } + if zb0038 { + (*z).AppParams = nil + } else if (*z).AppParams == nil { + (*z).AppParams = make(map[AppIndex]AppParams, zb0037) + } + for zb0037 > 0 { + var zb0007 AppIndex + var zb0008 AppParams + zb0037-- + bts, err = zb0007.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams") + return + } + bts, err = zb0008.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + (*z).AppParams[zb0007] = zb0008 + } + case "tsch": + var zb0039 int + var zb0040 bool + zb0039, zb0040, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0039, zb0040, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + if zb0039 > 0 { + zb0039-- + (*z).TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array", "NumUint") + return + } + } + if zb0039 > 0 { + zb0039-- + (*z).TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0039 > 0 { + err = msgp.ErrTooManyArrayFields(zb0039) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + if zb0040 { + (*z).TotalAppSchema = StateSchema{} + } + for zb0039 > 0 { + zb0039-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + switch string(field) { + case "nui": + (*z).TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "NumUint") + return + } + case "nbs": + (*z).TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + } + } + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -799,13 +1281,29 @@ func (z *AccountData) Msgsize() (s int) { s += 0 + zb0003.Msgsize() + 1 + 2 + msgp.Uint64Size + 2 + msgp.BoolSize } } - s += 6 + (*z).AuthAddr.Msgsize() + s += 6 + (*z).AuthAddr.Msgsize() + 5 + msgp.MapHeaderSize + if (*z).AppLocalStates != nil { + for zb0005, zb0006 := range (*z).AppLocalStates { + _ = zb0005 + _ = zb0006 + s += 0 + zb0005.Msgsize() + zb0006.Msgsize() + } + } + s += 5 + msgp.MapHeaderSize + if (*z).AppParams != nil { + for zb0007, zb0008 := range (*z).AppParams { + _ = zb0007 + _ = zb0008 + s += 0 + zb0007.Msgsize() + zb0008.Msgsize() + } + } + s += 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value func (z *AccountData) MsgIsZero() bool { - return ((*z).Status == 0) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) && ((*z).RewardedMicroAlgos.MsgIsZero()) && ((*z).VoteID.MsgIsZero()) && ((*z).SelectionID.MsgIsZero()) && ((*z).VoteFirstValid == 0) && ((*z).VoteLastValid == 0) && ((*z).VoteKeyDilution == 0) && (len((*z).AssetParams) == 0) && (len((*z).Assets) == 0) && ((*z).AuthAddr.MsgIsZero()) + return ((*z).Status == 0) && ((*z).MicroAlgos.MsgIsZero()) && ((*z).RewardsBase == 0) && ((*z).RewardedMicroAlgos.MsgIsZero()) && ((*z).VoteID.MsgIsZero()) && ((*z).SelectionID.MsgIsZero()) && ((*z).VoteFirstValid == 0) && ((*z).VoteLastValid == 0) && ((*z).VoteKeyDilution == 0) && (len((*z).AssetParams) == 0) && (len((*z).Assets) == 0) && ((*z).AuthAddr.MsgIsZero()) && (len((*z).AppLocalStates) == 0) && (len((*z).AppParams) == 0) && (((*z).TotalAppSchema.NumUint == 0) && ((*z).TotalAppSchema.NumByteSlice == 0)) } // MarshalMsg implements msgp.Marshaler @@ -837,72 +1335,251 @@ func (z *Address) MsgIsZero() bool { } // MarshalMsg implements msgp.Marshaler -func (z *AssetHolding) MarshalMsg(b []byte) (o []byte, err error) { +func (z AppIndex) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(2) - var zb0001Mask uint8 /* 3 bits */ - if (*z).Amount == 0 { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).Frozen == false { - zb0001Len-- - zb0001Mask |= 0x4 - } - // variable map header, size zb0001Len - o = append(o, 0x80|uint8(zb0001Len)) - if zb0001Len != 0 { - if (zb0001Mask & 0x2) == 0 { // if not empty - // string "a" - o = append(o, 0xa1, 0x61) - o = msgp.AppendUint64(o, (*z).Amount) - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "f" - o = append(o, 0xa1, 0x66) - o = msgp.AppendBool(o, (*z).Frozen) - } - } + o = msgp.AppendUint64(o, uint64(z)) return } -func (_ *AssetHolding) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*AssetHolding) +func (_ AppIndex) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(AppIndex) + if !ok { + _, ok = (z).(*AppIndex) + } return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *AssetHolding) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0001 int - var zb0002 bool - zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) +func (z *AppIndex) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = AppIndex(zb0001) + } + o = bts + return +} + +func (_ *AppIndex) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AppIndex) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z AppIndex) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z AppIndex) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *AppLocalState) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0003Len := uint32(2) + var zb0003Mask uint8 /* 3 bits */ + if ((*z).Schema.NumUint == 0) && ((*z).Schema.NumByteSlice == 0) { + zb0003Len-- + zb0003Mask |= 0x2 + } + if len((*z).KeyValue) == 0 { + zb0003Len-- + zb0003Mask |= 0x4 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if zb0003Len != 0 { + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "hsch" + o = append(o, 0xa4, 0x68, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0004Len := uint32(2) + var zb0004Mask uint8 /* 3 bits */ + if (*z).Schema.NumByteSlice == 0 { + zb0004Len-- + zb0004Mask |= 0x2 + } + if (*z).Schema.NumUint == 0 { + zb0004Len-- + zb0004Mask |= 0x4 + } + // variable map header, size zb0004Len + o = append(o, 0x80|uint8(zb0004Len)) + if (zb0004Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).Schema.NumByteSlice) + } + if (zb0004Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).Schema.NumUint) + } + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "tkv" + o = append(o, 0xa3, 0x74, 0x6b, 0x76) + if (*z).KeyValue == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).KeyValue))) + } + zb0001_keys := make([]string, 0, len((*z).KeyValue)) + for zb0001 := range (*z).KeyValue { + zb0001_keys = append(zb0001_keys, zb0001) + } + sort.Sort(SortString(zb0001_keys)) + for _, zb0001 := range zb0001_keys { + zb0002 := (*z).KeyValue[zb0001] + _ = zb0002 + o = msgp.AppendString(o, zb0001) + o, err = zb0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "KeyValue", zb0001) + return + } + } + } + } + return +} + +func (_ *AppLocalState) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*AppLocalState) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *AppLocalState) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0001 > 0 { - zb0001-- - (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Amount") - return + if zb0003 > 0 { + zb0003-- + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema") + return + } + if zb0005 > 0 { + zb0005-- + (*z).Schema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema", "struct-from-array", "NumUint") + return + } + } + if zb0005 > 0 { + zb0005-- + (*z).Schema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0005 > 0 { + err = msgp.ErrTooManyArrayFields(zb0005) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema") + return + } + if zb0006 { + (*z).Schema = StateSchema{} + } + for zb0005 > 0 { + zb0005-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema") + return + } + switch string(field) { + case "nui": + (*z).Schema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema", "NumUint") + return + } + case "nbs": + (*z).Schema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Schema") + return + } + } + } } } - if zb0001 > 0 { - zb0001-- - (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) + if zb0003 > 0 { + zb0003-- + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Frozen") + err = msgp.WrapError(err, "struct-from-array", "KeyValue") return } + if zb0007 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0007), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "struct-from-array", "KeyValue") + return + } + if zb0008 { + (*z).KeyValue = nil + } else if (*z).KeyValue == nil { + (*z).KeyValue = make(TealKeyValue, zb0007) + } + for zb0007 > 0 { + var zb0001 string + var zb0002 TealValue + zb0007-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KeyValue") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "KeyValue", zb0001) + return + } + (*z).KeyValue[zb0001] = zb0002 + } } - if zb0001 > 0 { - err = msgp.ErrTooManyArrayFields(zb0001) + if zb0003 > 0 { + err = msgp.ErrTooManyArrayFields(zb0003) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -913,29 +1590,121 @@ func (z *AssetHolding) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0002 { - (*z) = AssetHolding{} + if zb0004 { + (*z) = AppLocalState{} } - for zb0001 > 0 { - zb0001-- + for zb0003 > 0 { + zb0003-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) return } switch string(field) { - case "a": - (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) + case "hsch": + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Schema") + return + } + if zb0009 > 0 { + zb0009-- + (*z).Schema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Schema", "struct-from-array", "NumUint") + return + } + } + if zb0009 > 0 { + zb0009-- + (*z).Schema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Schema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) + if err != nil { + err = msgp.WrapError(err, "Schema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "Schema") + return + } + if zb0010 { + (*z).Schema = StateSchema{} + } + for zb0009 > 0 { + zb0009-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "Schema") + return + } + switch string(field) { + case "nui": + (*z).Schema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Schema", "NumUint") + return + } + case "nbs": + (*z).Schema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Schema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "Schema") + return + } + } + } + } + case "tkv": + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "Amount") + err = msgp.WrapError(err, "KeyValue") return } - case "f": - (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Frozen") + if zb0011 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "KeyValue") return } + if zb0012 { + (*z).KeyValue = nil + } else if (*z).KeyValue == nil { + (*z).KeyValue = make(TealKeyValue, zb0011) + } + for zb0011 > 0 { + var zb0001 string + var zb0002 TealValue + zb0011-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "KeyValue") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "KeyValue", zb0001) + return + } + (*z).KeyValue[zb0001] = zb0002 + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -949,390 +1718,572 @@ func (z *AssetHolding) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *AssetHolding) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*AssetHolding) +func (_ *AppLocalState) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AppLocalState) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *AssetHolding) Msgsize() (s int) { - s = 1 + 2 + msgp.Uint64Size + 2 + msgp.BoolSize +func (z *AppLocalState) Msgsize() (s int) { + s = 1 + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 4 + msgp.MapHeaderSize + if (*z).KeyValue != nil { + for zb0001, zb0002 := range (*z).KeyValue { + _ = zb0001 + _ = zb0002 + s += 0 + msgp.StringPrefixSize + len(zb0001) + zb0002.Msgsize() + } + } return } // MsgIsZero returns whether this is a zero value -func (z *AssetHolding) MsgIsZero() bool { - return ((*z).Amount == 0) && ((*z).Frozen == false) +func (z *AppLocalState) MsgIsZero() bool { + return (((*z).Schema.NumUint == 0) && ((*z).Schema.NumByteSlice == 0)) && (len((*z).KeyValue) == 0) } // MarshalMsg implements msgp.Marshaler -func (z AssetIndex) MarshalMsg(b []byte) (o []byte, err error) { +func (z *AppParams) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendUint64(o, uint64(z)) + // omitempty: check for empty values + zb0003Len := uint32(5) + var zb0003Mask uint8 /* 6 bits */ + if len((*z).ApprovalProgram) == 0 { + zb0003Len-- + zb0003Mask |= 0x2 + } + if len((*z).ClearStateProgram) == 0 { + zb0003Len-- + zb0003Mask |= 0x4 + } + if len((*z).GlobalState) == 0 { + zb0003Len-- + zb0003Mask |= 0x8 + } + if ((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0) { + zb0003Len-- + zb0003Mask |= 0x10 + } + if ((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0) { + zb0003Len-- + zb0003Mask |= 0x20 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if zb0003Len != 0 { + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "approv" + o = append(o, 0xa6, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76) + o = msgp.AppendBytes(o, (*z).ApprovalProgram) + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "clearp" + o = append(o, 0xa6, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x70) + o = msgp.AppendBytes(o, (*z).ClearStateProgram) + } + if (zb0003Mask & 0x8) == 0 { // if not empty + // string "gs" + o = append(o, 0xa2, 0x67, 0x73) + if (*z).GlobalState == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).GlobalState))) + } + zb0001_keys := make([]string, 0, len((*z).GlobalState)) + for zb0001 := range (*z).GlobalState { + zb0001_keys = append(zb0001_keys, zb0001) + } + sort.Sort(SortString(zb0001_keys)) + for _, zb0001 := range zb0001_keys { + zb0002 := (*z).GlobalState[zb0001] + _ = zb0002 + o = msgp.AppendString(o, zb0001) + o, err = zb0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "GlobalState", zb0001) + return + } + } + } + if (zb0003Mask & 0x10) == 0 { // if not empty + // string "gsch" + o = append(o, 0xa4, 0x67, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0004Len := uint32(2) + var zb0004Mask uint8 /* 3 bits */ + if (*z).GlobalStateSchema.NumByteSlice == 0 { + zb0004Len-- + zb0004Mask |= 0x2 + } + if (*z).GlobalStateSchema.NumUint == 0 { + zb0004Len-- + zb0004Mask |= 0x4 + } + // variable map header, size zb0004Len + o = append(o, 0x80|uint8(zb0004Len)) + if (zb0004Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumByteSlice) + } + if (zb0004Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumUint) + } + } + if (zb0003Mask & 0x20) == 0 { // if not empty + // string "lsch" + o = append(o, 0xa4, 0x6c, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0005Len := uint32(2) + var zb0005Mask uint8 /* 3 bits */ + if (*z).LocalStateSchema.NumByteSlice == 0 { + zb0005Len-- + zb0005Mask |= 0x2 + } + if (*z).LocalStateSchema.NumUint == 0 { + zb0005Len-- + zb0005Mask |= 0x4 + } + // variable map header, size zb0005Len + o = append(o, 0x80|uint8(zb0005Len)) + if (zb0005Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumByteSlice) + } + if (zb0005Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumUint) + } + } + } return } -func (_ AssetIndex) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(AssetIndex) - if !ok { - _, ok = (z).(*AssetIndex) - } +func (_ *AppParams) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*AppParams) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *AssetIndex) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 uint64 - zb0001, bts, err = msgp.ReadUint64Bytes(bts) +func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - (*z) = AssetIndex(zb0001) - } - o = bts - return -} - -func (_ *AssetIndex) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*AssetIndex) - return ok -} - -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z AssetIndex) Msgsize() (s int) { - s = msgp.Uint64Size - return -} - -// MsgIsZero returns whether this is a zero value -func (z AssetIndex) MsgIsZero() bool { - return z == 0 -} - -// MarshalMsg implements msgp.Marshaler -func (z *AssetParams) MarshalMsg(b []byte) (o []byte, err error) { - o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0002Len := uint32(11) - var zb0002Mask uint16 /* 12 bits */ - if (*z).MetadataHash == ([32]byte{}) { - zb0002Len-- - zb0002Mask |= 0x2 - } - if (*z).AssetName == "" { - zb0002Len-- - zb0002Mask |= 0x4 - } - if (*z).URL == "" { - zb0002Len-- - zb0002Mask |= 0x8 - } - if (*z).Clawback.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x10 - } - if (*z).Decimals == 0 { - zb0002Len-- - zb0002Mask |= 0x20 - } - if (*z).DefaultFrozen == false { - zb0002Len-- - zb0002Mask |= 0x40 - } - if (*z).Freeze.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x80 - } - if (*z).Manager.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x100 - } - if (*z).Reserve.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x200 - } - if (*z).Total == 0 { - zb0002Len-- - zb0002Mask |= 0x400 - } - if (*z).UnitName == "" { - zb0002Len-- - zb0002Mask |= 0x800 - } - // variable map header, size zb0002Len - o = append(o, 0x80|uint8(zb0002Len)) - if zb0002Len != 0 { - if (zb0002Mask & 0x2) == 0 { // if not empty - // string "am" - o = append(o, 0xa2, 0x61, 0x6d) - o = msgp.AppendBytes(o, ((*z).MetadataHash)[:]) - } - if (zb0002Mask & 0x4) == 0 { // if not empty - // string "an" - o = append(o, 0xa2, 0x61, 0x6e) - o = msgp.AppendString(o, (*z).AssetName) - } - if (zb0002Mask & 0x8) == 0 { // if not empty - // string "au" - o = append(o, 0xa2, 0x61, 0x75) - o = msgp.AppendString(o, (*z).URL) - } - if (zb0002Mask & 0x10) == 0 { // if not empty - // string "c" - o = append(o, 0xa1, 0x63) - o, err = (*z).Clawback.MarshalMsg(o) + if zb0003 > 0 { + zb0003-- + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { - err = msgp.WrapError(err, "Clawback") + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") return } } - if (zb0002Mask & 0x20) == 0 { // if not empty - // string "dc" - o = append(o, 0xa2, 0x64, 0x63) - o = msgp.AppendUint32(o, (*z).Decimals) - } - if (zb0002Mask & 0x40) == 0 { // if not empty - // string "df" - o = append(o, 0xa2, 0x64, 0x66) - o = msgp.AppendBool(o, (*z).DefaultFrozen) - } - if (zb0002Mask & 0x80) == 0 { // if not empty - // string "f" - o = append(o, 0xa1, 0x66) - o, err = (*z).Freeze.MarshalMsg(o) + if zb0003 > 0 { + zb0003-- + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { - err = msgp.WrapError(err, "Freeze") + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") return } } - if (zb0002Mask & 0x100) == 0 { // if not empty - // string "m" - o = append(o, 0xa1, 0x6d) - o, err = (*z).Manager.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Manager") - return + if zb0003 > 0 { + zb0003-- + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + if zb0005 > 0 { + zb0005-- + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0005 > 0 { + zb0005-- + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0005 > 0 { + err = msgp.ErrTooManyArrayFields(zb0005) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + if zb0006 { + (*z).LocalStateSchema = StateSchema{} + } + for zb0005 > 0 { + zb0005-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumUint") + return + } + case "nbs": + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + } + } } } - if (zb0002Mask & 0x200) == 0 { // if not empty - // string "r" - o = append(o, 0xa1, 0x72) - o, err = (*z).Reserve.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Reserve") - return + if zb0003 > 0 { + zb0003-- + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + if zb0007 > 0 { + zb0007-- + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0007 > 0 { + zb0007-- + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0007 > 0 { + err = msgp.ErrTooManyArrayFields(zb0007) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + if zb0008 { + (*z).GlobalStateSchema = StateSchema{} + } + for zb0007 > 0 { + zb0007-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumUint") + return + } + case "nbs": + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + } + } } } - if (zb0002Mask & 0x400) == 0 { // if not empty - // string "t" - o = append(o, 0xa1, 0x74) - o = msgp.AppendUint64(o, (*z).Total) - } - if (zb0002Mask & 0x800) == 0 { // if not empty - // string "un" - o = append(o, 0xa2, 0x75, 0x6e) - o = msgp.AppendString(o, (*z).UnitName) - } - } - return -} - -func (_ *AssetParams) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*AssetParams) - return ok -} - -// UnmarshalMsg implements msgp.Unmarshaler -func (z *AssetParams) UnmarshalMsg(bts []byte) (o []byte, err error) { - var field []byte - _ = field - var zb0002 int - var zb0003 bool - zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err) - return - } - if zb0002 > 0 { - zb0002-- - (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) + if zb0003 > 0 { + zb0003-- + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Total") + err = msgp.WrapError(err, "struct-from-array", "GlobalState") return } - } - if zb0002 > 0 { - zb0002-- - (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Decimals") + if zb0009 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "struct-from-array", "GlobalState") return } - } - if zb0002 > 0 { - zb0002-- - (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "DefaultFrozen") - return + if zb0010 { + (*z).GlobalState = nil + } else if (*z).GlobalState == nil { + (*z).GlobalState = make(TealKeyValue, zb0009) } - } - if zb0002 > 0 { - zb0002-- - (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "UnitName") - return + for zb0009 > 0 { + var zb0001 string + var zb0002 TealValue + zb0009-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState", zb0001) + return + } + (*z).GlobalState[zb0001] = zb0002 } } - if zb0002 > 0 { - zb0002-- - (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) + if zb0003 > 0 { + err = msgp.ErrTooManyArrayFields(zb0003) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AssetName") + err = msgp.WrapError(err, "struct-from-array") return } } - if zb0002 > 0 { - zb0002-- - (*z).URL, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "URL") - return - } + } else { + if err != nil { + err = msgp.WrapError(err) + return } - if zb0002 > 0 { - zb0002-- - bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) + if zb0004 { + (*z) = AppParams{} + } + for zb0003 > 0 { + zb0003-- + field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "MetadataHash") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).Manager.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Manager") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).Reserve.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Reserve") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).Freeze.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Freeze") - return - } - } - if zb0002 > 0 { - zb0002-- - bts, err = (*z).Clawback.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Clawback") - return - } - } - if zb0002 > 0 { - err = msgp.ErrTooManyArrayFields(zb0002) - if err != nil { - err = msgp.WrapError(err, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err) - return - } - if zb0003 { - (*z) = AssetParams{} - } - for zb0002 > 0 { - zb0002-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err) + err = msgp.WrapError(err) return } switch string(field) { - case "t": - (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Total") - return - } - case "dc": - (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Decimals") - return - } - case "df": - (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "DefaultFrozen") - return - } - case "un": - (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) + case "approv": + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { - err = msgp.WrapError(err, "UnitName") + err = msgp.WrapError(err, "ApprovalProgram") return } - case "an": - (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) + case "clearp": + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { - err = msgp.WrapError(err, "AssetName") + err = msgp.WrapError(err, "ClearStateProgram") return } - case "au": - (*z).URL, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "URL") - return + case "lsch": + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + if zb0011 > 0 { + zb0011-- + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0011 > 0 { + zb0011-- + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0011 > 0 { + err = msgp.ErrTooManyArrayFields(zb0011) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + if zb0012 { + (*z).LocalStateSchema = StateSchema{} + } + for zb0011 > 0 { + zb0011-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "NumUint") + return + } + case "nbs": + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + } + } } - case "am": - bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) - if err != nil { - err = msgp.WrapError(err, "MetadataHash") - return + case "gsch": + var zb0013 int + var zb0014 bool + zb0013, zb0014, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0013, zb0014, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + if zb0013 > 0 { + zb0013-- + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0013 > 0 { + zb0013-- + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0013 > 0 { + err = msgp.ErrTooManyArrayFields(zb0013) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + if zb0014 { + (*z).GlobalStateSchema = StateSchema{} + } + for zb0013 > 0 { + zb0013-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "NumUint") + return + } + case "nbs": + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + } + } } - case "m": - bts, err = (*z).Manager.UnmarshalMsg(bts) + case "gs": + var zb0015 int + var zb0016 bool + zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "Manager") + err = msgp.WrapError(err, "GlobalState") return } - case "r": - bts, err = (*z).Reserve.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Reserve") + if zb0015 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0015), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "GlobalState") return } - case "f": - bts, err = (*z).Freeze.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Freeze") - return + if zb0016 { + (*z).GlobalState = nil + } else if (*z).GlobalState == nil { + (*z).GlobalState = make(TealKeyValue, zb0015) } - case "c": - bts, err = (*z).Clawback.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Clawback") - return + for zb0015 > 0 { + var zb0001 string + var zb0002 TealValue + zb0015-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState", zb0001) + return + } + (*z).GlobalState[zb0001] = zb0002 } default: err = msgp.ErrNoField(string(field)) @@ -1347,498 +2298,2466 @@ func (z *AssetParams) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *AssetParams) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*AssetParams) +func (_ *AppParams) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AppParams) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *AssetParams) Msgsize() (s int) { - s = 1 + 2 + msgp.Uint64Size + 3 + msgp.Uint32Size + 3 + msgp.BoolSize + 3 + msgp.StringPrefixSize + len((*z).UnitName) + 3 + msgp.StringPrefixSize + len((*z).AssetName) + 3 + msgp.StringPrefixSize + len((*z).URL) + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + (*z).Manager.Msgsize() + 2 + (*z).Reserve.Msgsize() + 2 + (*z).Freeze.Msgsize() + 2 + (*z).Clawback.Msgsize() +func (z *AppParams) Msgsize() (s int) { + s = 1 + 7 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 7 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 3 + msgp.MapHeaderSize + if (*z).GlobalState != nil { + for zb0001, zb0002 := range (*z).GlobalState { + _ = zb0001 + _ = zb0002 + s += 0 + msgp.StringPrefixSize + len(zb0001) + zb0002.Msgsize() + } + } return } // MsgIsZero returns whether this is a zero value -func (z *AssetParams) MsgIsZero() bool { - return ((*z).Total == 0) && ((*z).Decimals == 0) && ((*z).DefaultFrozen == false) && ((*z).UnitName == "") && ((*z).AssetName == "") && ((*z).URL == "") && ((*z).MetadataHash == ([32]byte{})) && ((*z).Manager.MsgIsZero()) && ((*z).Reserve.MsgIsZero()) && ((*z).Freeze.MsgIsZero()) && ((*z).Clawback.MsgIsZero()) +func (z *AppParams) MsgIsZero() bool { + return (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) && (((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0)) && (((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0)) && (len((*z).GlobalState) == 0) } // MarshalMsg implements msgp.Marshaler -func (z *BalanceRecord) MarshalMsg(b []byte) (o []byte, err error) { +func (z *AssetHolding) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0005Len := uint32(13) - var zb0005Mask uint16 /* 15 bits */ - if (*z).Addr.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x4 - } - if (*z).AccountData.MicroAlgos.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x8 - } - if len((*z).AccountData.AssetParams) == 0 { - zb0005Len-- - zb0005Mask |= 0x10 - } - if len((*z).AccountData.Assets) == 0 { - zb0005Len-- - zb0005Mask |= 0x20 - } - if (*z).AccountData.RewardsBase == 0 { - zb0005Len-- - zb0005Mask |= 0x40 - } - if (*z).AccountData.RewardedMicroAlgos.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x80 - } - if (*z).AccountData.Status == 0 { - zb0005Len-- - zb0005Mask |= 0x100 - } - if (*z).AccountData.SelectionID.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x200 - } - if (*z).AccountData.AuthAddr.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x400 - } - if (*z).AccountData.VoteID.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x800 - } - if (*z).AccountData.VoteFirstValid == 0 { - zb0005Len-- - zb0005Mask |= 0x1000 - } - if (*z).AccountData.VoteKeyDilution == 0 { - zb0005Len-- - zb0005Mask |= 0x2000 + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).Amount == 0 { + zb0001Len-- + zb0001Mask |= 0x2 } - if (*z).AccountData.VoteLastValid == 0 { - zb0005Len-- - zb0005Mask |= 0x4000 + if (*z).Frozen == false { + zb0001Len-- + zb0001Mask |= 0x4 } - // variable map header, size zb0005Len - o = append(o, 0x80|uint8(zb0005Len)) - if zb0005Len != 0 { - if (zb0005Mask & 0x4) == 0 { // if not empty - // string "addr" - o = append(o, 0xa4, 0x61, 0x64, 0x64, 0x72) - o, err = (*z).Addr.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Addr") - return - } - } - if (zb0005Mask & 0x8) == 0 { // if not empty - // string "algo" - o = append(o, 0xa4, 0x61, 0x6c, 0x67, 0x6f) - o, err = (*z).AccountData.MicroAlgos.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "MicroAlgos") - return - } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = msgp.AppendUint64(o, (*z).Amount) } - if (zb0005Mask & 0x10) == 0 { // if not empty - // string "apar" - o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x72) - if (*z).AccountData.AssetParams == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.AssetParams))) - } - zb0001_keys := make([]AssetIndex, 0, len((*z).AccountData.AssetParams)) - for zb0001 := range (*z).AccountData.AssetParams { - zb0001_keys = append(zb0001_keys, zb0001) - } - sort.Sort(SortAssetIndex(zb0001_keys)) - for _, zb0001 := range zb0001_keys { - zb0002 := (*z).AccountData.AssetParams[zb0001] - _ = zb0002 - o, err = zb0001.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "AssetParams", zb0001) - return - } - o, err = zb0002.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "AssetParams", zb0001) - return - } - } - } - if (zb0005Mask & 0x20) == 0 { // if not empty - // string "asset" - o = append(o, 0xa5, 0x61, 0x73, 0x73, 0x65, 0x74) - if (*z).AccountData.Assets == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.Assets))) - } - zb0003_keys := make([]AssetIndex, 0, len((*z).AccountData.Assets)) - for zb0003 := range (*z).AccountData.Assets { - zb0003_keys = append(zb0003_keys, zb0003) - } - sort.Sort(SortAssetIndex(zb0003_keys)) - for _, zb0003 := range zb0003_keys { - zb0004 := (*z).AccountData.Assets[zb0003] - _ = zb0004 - o, err = zb0003.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003) - return - } - // omitempty: check for empty values - zb0006Len := uint32(2) - var zb0006Mask uint8 /* 3 bits */ - if zb0004.Amount == 0 { - zb0006Len-- - zb0006Mask |= 0x2 - } - if zb0004.Frozen == false { - zb0006Len-- - zb0006Mask |= 0x4 - } - // variable map header, size zb0006Len - o = append(o, 0x80|uint8(zb0006Len)) - if zb0006Len != 0 { - if (zb0006Mask & 0x2) == 0 { // if not empty - // string "a" - o = append(o, 0xa1, 0x61) - o = msgp.AppendUint64(o, zb0004.Amount) - } - if (zb0006Mask & 0x4) == 0 { // if not empty - // string "f" - o = append(o, 0xa1, 0x66) - o = msgp.AppendBool(o, zb0004.Frozen) - } - } - } - } - if (zb0005Mask & 0x40) == 0 { // if not empty - // string "ebase" - o = append(o, 0xa5, 0x65, 0x62, 0x61, 0x73, 0x65) - o = msgp.AppendUint64(o, (*z).AccountData.RewardsBase) - } - if (zb0005Mask & 0x80) == 0 { // if not empty - // string "ern" - o = append(o, 0xa3, 0x65, 0x72, 0x6e) - o, err = (*z).AccountData.RewardedMicroAlgos.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "RewardedMicroAlgos") - return - } - } - if (zb0005Mask & 0x100) == 0 { // if not empty - // string "onl" - o = append(o, 0xa3, 0x6f, 0x6e, 0x6c) - o = msgp.AppendByte(o, byte((*z).AccountData.Status)) - } - if (zb0005Mask & 0x200) == 0 { // if not empty - // string "sel" - o = append(o, 0xa3, 0x73, 0x65, 0x6c) - o, err = (*z).AccountData.SelectionID.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "SelectionID") - return - } - } - if (zb0005Mask & 0x400) == 0 { // if not empty - // string "spend" - o = append(o, 0xa5, 0x73, 0x70, 0x65, 0x6e, 0x64) - o, err = (*z).AccountData.AuthAddr.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "AuthAddr") - return - } - } - if (zb0005Mask & 0x800) == 0 { // if not empty - // string "vote" - o = append(o, 0xa4, 0x76, 0x6f, 0x74, 0x65) - o, err = (*z).AccountData.VoteID.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "VoteID") - return - } - } - if (zb0005Mask & 0x1000) == 0 { // if not empty - // string "voteFst" - o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x46, 0x73, 0x74) - o = msgp.AppendUint64(o, uint64((*z).AccountData.VoteFirstValid)) - } - if (zb0005Mask & 0x2000) == 0 { // if not empty - // string "voteKD" - o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x4b, 0x44) - o = msgp.AppendUint64(o, (*z).AccountData.VoteKeyDilution) - } - if (zb0005Mask & 0x4000) == 0 { // if not empty - // string "voteLst" - o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x4c, 0x73, 0x74) - o = msgp.AppendUint64(o, uint64((*z).AccountData.VoteLastValid)) + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "f" + o = append(o, 0xa1, 0x66) + o = msgp.AppendBool(o, (*z).Frozen) } } return } -func (_ *BalanceRecord) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*BalanceRecord) +func (_ *AssetHolding) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*AssetHolding) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *BalanceRecord) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *AssetHolding) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).Addr.UnmarshalMsg(bts) + if zb0001 > 0 { + zb0001-- + (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Addr") + err = msgp.WrapError(err, "struct-from-array", "Amount") return } } - if zb0005 > 0 { - zb0005-- - { - var zb0007 byte - zb0007, bts, err = msgp.ReadByteBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Status") - return - } - (*z).AccountData.Status = Status(zb0007) - } - } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).AccountData.MicroAlgos.UnmarshalMsg(bts) + if zb0001 > 0 { + zb0001-- + (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") + err = msgp.WrapError(err, "struct-from-array", "Frozen") return } } - if zb0005 > 0 { - zb0005-- - (*z).AccountData.RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "RewardsBase") + err = msgp.WrapError(err, "struct-from-array") return } } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).AccountData.RewardedMicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "RewardedMicroAlgos") - return - } + } else { + if err != nil { + err = msgp.WrapError(err) + return } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).AccountData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteID") - return - } + if zb0002 { + (*z) = AssetHolding{} } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).AccountData.SelectionID.UnmarshalMsg(bts) + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "SelectionID") + err = msgp.WrapError(err) return } - } - if zb0005 > 0 { - zb0005-- - { - var zb0008 uint64 - zb0008, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") - return - } - (*z).AccountData.VoteFirstValid = Round(zb0008) - } - } - if zb0005 > 0 { - zb0005-- - { - var zb0009 uint64 - zb0009, bts, err = msgp.ReadUint64Bytes(bts) + switch string(field) { + case "a": + (*z).Amount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") + err = msgp.WrapError(err, "Amount") return } - (*z).AccountData.VoteLastValid = Round(zb0009) - } - } - if zb0005 > 0 { - zb0005-- - (*z).AccountData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") - return - } - } - if zb0005 > 0 { - zb0005-- - var zb0010 int - var zb0011 bool - zb0010, zb0011, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AssetParams") - return - } - if zb0010 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxAssetsPerAccount)) - err = msgp.WrapError(err, "struct-from-array", "AssetParams") - return - } - if zb0011 { - (*z).AccountData.AssetParams = nil - } else if (*z).AccountData.AssetParams == nil { - (*z).AccountData.AssetParams = make(map[AssetIndex]AssetParams, zb0010) - } - for zb0010 > 0 { - var zb0001 AssetIndex - var zb0002 AssetParams - zb0010-- - bts, err = zb0001.UnmarshalMsg(bts) + case "f": + (*z).Frozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AssetParams") + err = msgp.WrapError(err, "Frozen") return } - bts, err = zb0002.UnmarshalMsg(bts) + default: + err = msgp.ErrNoField(string(field)) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AssetParams", zb0001) + err = msgp.WrapError(err) return } - (*z).AccountData.AssetParams[zb0001] = zb0002 } } - if zb0005 > 0 { - zb0005-- - var zb0012 int - var zb0013 bool - zb0012, zb0013, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets") - return - } - if zb0012 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxAssetsPerAccount)) - err = msgp.WrapError(err, "struct-from-array", "Assets") - return - } - if zb0013 { - (*z).AccountData.Assets = nil - } else if (*z).AccountData.Assets == nil { - (*z).AccountData.Assets = make(map[AssetIndex]AssetHolding, zb0012) - } - for zb0012 > 0 { - var zb0003 AssetIndex - var zb0004 AssetHolding - zb0012-- - bts, err = zb0003.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets") - return - } - var zb0014 int - var zb0015 bool - zb0014, zb0015, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) - return - } - if zb0014 > 0 { - zb0014-- - zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Amount") - return - } - } - if zb0014 > 0 { - zb0014-- - zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Frozen") - return - } - } - if zb0014 > 0 { - err = msgp.ErrTooManyArrayFields(zb0014) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array") - return + } + o = bts + return +} + +func (_ *AssetHolding) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AssetHolding) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *AssetHolding) Msgsize() (s int) { + s = 1 + 2 + msgp.Uint64Size + 2 + msgp.BoolSize + return +} + +// MsgIsZero returns whether this is a zero value +func (z *AssetHolding) MsgIsZero() bool { + return ((*z).Amount == 0) && ((*z).Frozen == false) +} + +// MarshalMsg implements msgp.Marshaler +func (z AssetIndex) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ AssetIndex) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(AssetIndex) + if !ok { + _, ok = (z).(*AssetIndex) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *AssetIndex) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = AssetIndex(zb0001) + } + o = bts + return +} + +func (_ *AssetIndex) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AssetIndex) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z AssetIndex) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z AssetIndex) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *AssetParams) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0002Len := uint32(11) + var zb0002Mask uint16 /* 12 bits */ + if (*z).MetadataHash == ([32]byte{}) { + zb0002Len-- + zb0002Mask |= 0x2 + } + if (*z).AssetName == "" { + zb0002Len-- + zb0002Mask |= 0x4 + } + if (*z).URL == "" { + zb0002Len-- + zb0002Mask |= 0x8 + } + if (*z).Clawback.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x10 + } + if (*z).Decimals == 0 { + zb0002Len-- + zb0002Mask |= 0x20 + } + if (*z).DefaultFrozen == false { + zb0002Len-- + zb0002Mask |= 0x40 + } + if (*z).Freeze.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x80 + } + if (*z).Manager.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x100 + } + if (*z).Reserve.MsgIsZero() { + zb0002Len-- + zb0002Mask |= 0x200 + } + if (*z).Total == 0 { + zb0002Len-- + zb0002Mask |= 0x400 + } + if (*z).UnitName == "" { + zb0002Len-- + zb0002Mask |= 0x800 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "am" + o = append(o, 0xa2, 0x61, 0x6d) + o = msgp.AppendBytes(o, ((*z).MetadataHash)[:]) + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "an" + o = append(o, 0xa2, 0x61, 0x6e) + o = msgp.AppendString(o, (*z).AssetName) + } + if (zb0002Mask & 0x8) == 0 { // if not empty + // string "au" + o = append(o, 0xa2, 0x61, 0x75) + o = msgp.AppendString(o, (*z).URL) + } + if (zb0002Mask & 0x10) == 0 { // if not empty + // string "c" + o = append(o, 0xa1, 0x63) + o, err = (*z).Clawback.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Clawback") + return + } + } + if (zb0002Mask & 0x20) == 0 { // if not empty + // string "dc" + o = append(o, 0xa2, 0x64, 0x63) + o = msgp.AppendUint32(o, (*z).Decimals) + } + if (zb0002Mask & 0x40) == 0 { // if not empty + // string "df" + o = append(o, 0xa2, 0x64, 0x66) + o = msgp.AppendBool(o, (*z).DefaultFrozen) + } + if (zb0002Mask & 0x80) == 0 { // if not empty + // string "f" + o = append(o, 0xa1, 0x66) + o, err = (*z).Freeze.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Freeze") + return + } + } + if (zb0002Mask & 0x100) == 0 { // if not empty + // string "m" + o = append(o, 0xa1, 0x6d) + o, err = (*z).Manager.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Manager") + return + } + } + if (zb0002Mask & 0x200) == 0 { // if not empty + // string "r" + o = append(o, 0xa1, 0x72) + o, err = (*z).Reserve.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Reserve") + return + } + } + if (zb0002Mask & 0x400) == 0 { // if not empty + // string "t" + o = append(o, 0xa1, 0x74) + o = msgp.AppendUint64(o, (*z).Total) + } + if (zb0002Mask & 0x800) == 0 { // if not empty + // string "un" + o = append(o, 0xa2, 0x75, 0x6e) + o = msgp.AppendString(o, (*z).UnitName) + } + } + return +} + +func (_ *AssetParams) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*AssetParams) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *AssetParams) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0002 int + var zb0003 bool + zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 > 0 { + zb0002-- + (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Total") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Decimals") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "DefaultFrozen") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "UnitName") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AssetName") + return + } + } + if zb0002 > 0 { + zb0002-- + (*z).URL, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "URL") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "MetadataHash") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Manager.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Manager") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Reserve.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Reserve") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Freeze.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Freeze") + return + } + } + if zb0002 > 0 { + zb0002-- + bts, err = (*z).Clawback.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Clawback") + return + } + } + if zb0002 > 0 { + err = msgp.ErrTooManyArrayFields(zb0002) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 { + (*z) = AssetParams{} + } + for zb0002 > 0 { + zb0002-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "t": + (*z).Total, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Total") + return + } + case "dc": + (*z).Decimals, bts, err = msgp.ReadUint32Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Decimals") + return + } + case "df": + (*z).DefaultFrozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "DefaultFrozen") + return + } + case "un": + (*z).UnitName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "UnitName") + return + } + case "an": + (*z).AssetName, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AssetName") + return + } + case "au": + (*z).URL, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "URL") + return + } + case "am": + bts, err = msgp.ReadExactBytes(bts, ((*z).MetadataHash)[:]) + if err != nil { + err = msgp.WrapError(err, "MetadataHash") + return + } + case "m": + bts, err = (*z).Manager.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Manager") + return + } + case "r": + bts, err = (*z).Reserve.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Reserve") + return + } + case "f": + bts, err = (*z).Freeze.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Freeze") + return + } + case "c": + bts, err = (*z).Clawback.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Clawback") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *AssetParams) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*AssetParams) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *AssetParams) Msgsize() (s int) { + s = 1 + 2 + msgp.Uint64Size + 3 + msgp.Uint32Size + 3 + msgp.BoolSize + 3 + msgp.StringPrefixSize + len((*z).UnitName) + 3 + msgp.StringPrefixSize + len((*z).AssetName) + 3 + msgp.StringPrefixSize + len((*z).URL) + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 2 + (*z).Manager.Msgsize() + 2 + (*z).Reserve.Msgsize() + 2 + (*z).Freeze.Msgsize() + 2 + (*z).Clawback.Msgsize() + return +} + +// MsgIsZero returns whether this is a zero value +func (z *AssetParams) MsgIsZero() bool { + return ((*z).Total == 0) && ((*z).Decimals == 0) && ((*z).DefaultFrozen == false) && ((*z).UnitName == "") && ((*z).AssetName == "") && ((*z).URL == "") && ((*z).MetadataHash == ([32]byte{})) && ((*z).Manager.MsgIsZero()) && ((*z).Reserve.MsgIsZero()) && ((*z).Freeze.MsgIsZero()) && ((*z).Clawback.MsgIsZero()) +} + +// MarshalMsg implements msgp.Marshaler +func (z *BalanceRecord) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0009Len := uint32(16) + var zb0009Mask uint32 /* 18 bits */ + if (*z).Addr.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x4 + } + if (*z).AccountData.MicroAlgos.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x8 + } + if len((*z).AccountData.AssetParams) == 0 { + zb0009Len-- + zb0009Mask |= 0x10 + } + if len((*z).AccountData.AppLocalStates) == 0 { + zb0009Len-- + zb0009Mask |= 0x20 + } + if len((*z).AccountData.AppParams) == 0 { + zb0009Len-- + zb0009Mask |= 0x40 + } + if len((*z).AccountData.Assets) == 0 { + zb0009Len-- + zb0009Mask |= 0x80 + } + if (*z).AccountData.RewardsBase == 0 { + zb0009Len-- + zb0009Mask |= 0x100 + } + if (*z).AccountData.RewardedMicroAlgos.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x200 + } + if (*z).AccountData.Status == 0 { + zb0009Len-- + zb0009Mask |= 0x400 + } + if (*z).AccountData.SelectionID.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x800 + } + if (*z).AccountData.AuthAddr.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x1000 + } + if ((*z).AccountData.TotalAppSchema.NumUint == 0) && ((*z).AccountData.TotalAppSchema.NumByteSlice == 0) { + zb0009Len-- + zb0009Mask |= 0x2000 + } + if (*z).AccountData.VoteID.MsgIsZero() { + zb0009Len-- + zb0009Mask |= 0x4000 + } + if (*z).AccountData.VoteFirstValid == 0 { + zb0009Len-- + zb0009Mask |= 0x8000 + } + if (*z).AccountData.VoteKeyDilution == 0 { + zb0009Len-- + zb0009Mask |= 0x10000 + } + if (*z).AccountData.VoteLastValid == 0 { + zb0009Len-- + zb0009Mask |= 0x20000 + } + // variable map header, size zb0009Len + o = msgp.AppendMapHeader(o, zb0009Len) + if zb0009Len != 0 { + if (zb0009Mask & 0x4) == 0 { // if not empty + // string "addr" + o = append(o, 0xa4, 0x61, 0x64, 0x64, 0x72) + o, err = (*z).Addr.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Addr") + return + } + } + if (zb0009Mask & 0x8) == 0 { // if not empty + // string "algo" + o = append(o, 0xa4, 0x61, 0x6c, 0x67, 0x6f) + o, err = (*z).AccountData.MicroAlgos.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "MicroAlgos") + return + } + } + if (zb0009Mask & 0x10) == 0 { // if not empty + // string "apar" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x72) + if (*z).AccountData.AssetParams == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.AssetParams))) + } + zb0001_keys := make([]AssetIndex, 0, len((*z).AccountData.AssetParams)) + for zb0001 := range (*z).AccountData.AssetParams { + zb0001_keys = append(zb0001_keys, zb0001) + } + sort.Sort(SortAssetIndex(zb0001_keys)) + for _, zb0001 := range zb0001_keys { + zb0002 := (*z).AccountData.AssetParams[zb0001] + _ = zb0002 + o, err = zb0001.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AssetParams", zb0001) + return + } + o, err = zb0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AssetParams", zb0001) + return + } + } + } + if (zb0009Mask & 0x20) == 0 { // if not empty + // string "appl" + o = append(o, 0xa4, 0x61, 0x70, 0x70, 0x6c) + if (*z).AccountData.AppLocalStates == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.AppLocalStates))) + } + zb0005_keys := make([]AppIndex, 0, len((*z).AccountData.AppLocalStates)) + for zb0005 := range (*z).AccountData.AppLocalStates { + zb0005_keys = append(zb0005_keys, zb0005) + } + sort.Sort(SortAppIndex(zb0005_keys)) + for _, zb0005 := range zb0005_keys { + zb0006 := (*z).AccountData.AppLocalStates[zb0005] + _ = zb0006 + o, err = zb0005.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + o, err = zb0006.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + } + } + if (zb0009Mask & 0x40) == 0 { // if not empty + // string "appp" + o = append(o, 0xa4, 0x61, 0x70, 0x70, 0x70) + if (*z).AccountData.AppParams == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.AppParams))) + } + zb0007_keys := make([]AppIndex, 0, len((*z).AccountData.AppParams)) + for zb0007 := range (*z).AccountData.AppParams { + zb0007_keys = append(zb0007_keys, zb0007) + } + sort.Sort(SortAppIndex(zb0007_keys)) + for _, zb0007 := range zb0007_keys { + zb0008 := (*z).AccountData.AppParams[zb0007] + _ = zb0008 + o, err = zb0007.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + o, err = zb0008.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + } + } + if (zb0009Mask & 0x80) == 0 { // if not empty + // string "asset" + o = append(o, 0xa5, 0x61, 0x73, 0x73, 0x65, 0x74) + if (*z).AccountData.Assets == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).AccountData.Assets))) + } + zb0003_keys := make([]AssetIndex, 0, len((*z).AccountData.Assets)) + for zb0003 := range (*z).AccountData.Assets { + zb0003_keys = append(zb0003_keys, zb0003) + } + sort.Sort(SortAssetIndex(zb0003_keys)) + for _, zb0003 := range zb0003_keys { + zb0004 := (*z).AccountData.Assets[zb0003] + _ = zb0004 + o, err = zb0003.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003) + return + } + // omitempty: check for empty values + zb0010Len := uint32(2) + var zb0010Mask uint8 /* 3 bits */ + if zb0004.Amount == 0 { + zb0010Len-- + zb0010Mask |= 0x2 + } + if zb0004.Frozen == false { + zb0010Len-- + zb0010Mask |= 0x4 + } + // variable map header, size zb0010Len + o = append(o, 0x80|uint8(zb0010Len)) + if zb0010Len != 0 { + if (zb0010Mask & 0x2) == 0 { // if not empty + // string "a" + o = append(o, 0xa1, 0x61) + o = msgp.AppendUint64(o, zb0004.Amount) + } + if (zb0010Mask & 0x4) == 0 { // if not empty + // string "f" + o = append(o, 0xa1, 0x66) + o = msgp.AppendBool(o, zb0004.Frozen) + } + } + } + } + if (zb0009Mask & 0x100) == 0 { // if not empty + // string "ebase" + o = append(o, 0xa5, 0x65, 0x62, 0x61, 0x73, 0x65) + o = msgp.AppendUint64(o, (*z).AccountData.RewardsBase) + } + if (zb0009Mask & 0x200) == 0 { // if not empty + // string "ern" + o = append(o, 0xa3, 0x65, 0x72, 0x6e) + o, err = (*z).AccountData.RewardedMicroAlgos.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "RewardedMicroAlgos") + return + } + } + if (zb0009Mask & 0x400) == 0 { // if not empty + // string "onl" + o = append(o, 0xa3, 0x6f, 0x6e, 0x6c) + o = msgp.AppendByte(o, byte((*z).AccountData.Status)) + } + if (zb0009Mask & 0x800) == 0 { // if not empty + // string "sel" + o = append(o, 0xa3, 0x73, 0x65, 0x6c) + o, err = (*z).AccountData.SelectionID.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "SelectionID") + return + } + } + if (zb0009Mask & 0x1000) == 0 { // if not empty + // string "spend" + o = append(o, 0xa5, 0x73, 0x70, 0x65, 0x6e, 0x64) + o, err = (*z).AccountData.AuthAddr.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "AuthAddr") + return + } + } + if (zb0009Mask & 0x2000) == 0 { // if not empty + // string "tsch" + o = append(o, 0xa4, 0x74, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0011Len := uint32(2) + var zb0011Mask uint8 /* 3 bits */ + if (*z).AccountData.TotalAppSchema.NumByteSlice == 0 { + zb0011Len-- + zb0011Mask |= 0x2 + } + if (*z).AccountData.TotalAppSchema.NumUint == 0 { + zb0011Len-- + zb0011Mask |= 0x4 + } + // variable map header, size zb0011Len + o = append(o, 0x80|uint8(zb0011Len)) + if (zb0011Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).AccountData.TotalAppSchema.NumByteSlice) + } + if (zb0011Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).AccountData.TotalAppSchema.NumUint) + } + } + if (zb0009Mask & 0x4000) == 0 { // if not empty + // string "vote" + o = append(o, 0xa4, 0x76, 0x6f, 0x74, 0x65) + o, err = (*z).AccountData.VoteID.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "VoteID") + return + } + } + if (zb0009Mask & 0x8000) == 0 { // if not empty + // string "voteFst" + o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x46, 0x73, 0x74) + o = msgp.AppendUint64(o, uint64((*z).AccountData.VoteFirstValid)) + } + if (zb0009Mask & 0x10000) == 0 { // if not empty + // string "voteKD" + o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x4b, 0x44) + o = msgp.AppendUint64(o, (*z).AccountData.VoteKeyDilution) + } + if (zb0009Mask & 0x20000) == 0 { // if not empty + // string "voteLst" + o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x4c, 0x73, 0x74) + o = msgp.AppendUint64(o, uint64((*z).AccountData.VoteLastValid)) + } + } + return +} + +func (_ *BalanceRecord) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*BalanceRecord) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *BalanceRecord) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).Addr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Addr") + return + } + } + if zb0009 > 0 { + zb0009-- + { + var zb0011 byte + zb0011, bts, err = msgp.ReadByteBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Status") + return + } + (*z).AccountData.Status = Status(zb0011) + } + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).AccountData.MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "MicroAlgos") + return + } + } + if zb0009 > 0 { + zb0009-- + (*z).AccountData.RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "RewardsBase") + return + } + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).AccountData.RewardedMicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "RewardedMicroAlgos") + return + } + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).AccountData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteID") + return + } + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).AccountData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "SelectionID") + return + } + } + if zb0009 > 0 { + zb0009-- + { + var zb0012 uint64 + zb0012, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteFirstValid") + return + } + (*z).AccountData.VoteFirstValid = Round(zb0012) + } + } + if zb0009 > 0 { + zb0009-- + { + var zb0013 uint64 + zb0013, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteLastValid") + return + } + (*z).AccountData.VoteLastValid = Round(zb0013) + } + } + if zb0009 > 0 { + zb0009-- + (*z).AccountData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") + return + } + } + if zb0009 > 0 { + zb0009-- + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AssetParams") + return + } + if zb0014 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxAssetsPerAccount)) + err = msgp.WrapError(err, "struct-from-array", "AssetParams") + return + } + if zb0015 { + (*z).AccountData.AssetParams = nil + } else if (*z).AccountData.AssetParams == nil { + (*z).AccountData.AssetParams = make(map[AssetIndex]AssetParams, zb0014) + } + for zb0014 > 0 { + var zb0001 AssetIndex + var zb0002 AssetParams + zb0014-- + bts, err = zb0001.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AssetParams") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AssetParams", zb0001) + return + } + (*z).AccountData.AssetParams[zb0001] = zb0002 + } + } + if zb0009 > 0 { + zb0009-- + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets") + return + } + if zb0016 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxAssetsPerAccount)) + err = msgp.WrapError(err, "struct-from-array", "Assets") + return + } + if zb0017 { + (*z).AccountData.Assets = nil + } else if (*z).AccountData.Assets == nil { + (*z).AccountData.Assets = make(map[AssetIndex]AssetHolding, zb0016) + } + for zb0016 > 0 { + var zb0003 AssetIndex + var zb0004 AssetHolding + zb0016-- + bts, err = zb0003.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets") + return + } + var zb0018 int + var zb0019 bool + zb0018, zb0019, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + return + } + if zb0018 > 0 { + zb0018-- + zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Amount") + return + } + } + if zb0018 > 0 { + zb0018-- + zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array", "Frozen") + return + } + } + if zb0018 > 0 { + err = msgp.ErrTooManyArrayFields(zb0018) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + return + } + if zb0019 { + zb0004 = AssetHolding{} + } + for zb0018 > 0 { + zb0018-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + return + } + switch string(field) { + case "a": + zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "Amount") + return + } + case "f": + zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "Frozen") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + return + } + } + } + } + (*z).AccountData.Assets[zb0003] = zb0004 + } + } + if zb0009 > 0 { + zb0009-- + bts, err = (*z).AccountData.AuthAddr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AuthAddr") + return + } + } + if zb0009 > 0 { + zb0009-- + var zb0020 int + var zb0021 bool + zb0020, zb0021, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + if zb0020 > encodedMaxAppLocalStates { + err = msgp.ErrOverflow(uint64(zb0020), uint64(encodedMaxAppLocalStates)) + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + if zb0021 { + (*z).AccountData.AppLocalStates = nil + } else if (*z).AccountData.AppLocalStates == nil { + (*z).AccountData.AppLocalStates = make(map[AppIndex]AppLocalState, zb0020) + } + for zb0020 > 0 { + var zb0005 AppIndex + var zb0006 AppLocalState + zb0020-- + bts, err = zb0005.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates") + return + } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppLocalStates", zb0005) + return + } + (*z).AccountData.AppLocalStates[zb0005] = zb0006 + } + } + if zb0009 > 0 { + zb0009-- + var zb0022 int + var zb0023 bool + zb0022, zb0023, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + if zb0022 > encodedMaxAppParams { + err = msgp.ErrOverflow(uint64(zb0022), uint64(encodedMaxAppParams)) + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + if zb0023 { + (*z).AccountData.AppParams = nil + } else if (*z).AccountData.AppParams == nil { + (*z).AccountData.AppParams = make(map[AppIndex]AppParams, zb0022) + } + for zb0022 > 0 { + var zb0007 AppIndex + var zb0008 AppParams + zb0022-- + bts, err = zb0007.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams") + return + } + bts, err = zb0008.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "AppParams", zb0007) + return + } + (*z).AccountData.AppParams[zb0007] = zb0008 + } + } + if zb0009 > 0 { + zb0009-- + var zb0024 int + var zb0025 bool + zb0024, zb0025, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0024, zb0025, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + if zb0024 > 0 { + zb0024-- + (*z).AccountData.TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array", "NumUint") + return + } + } + if zb0024 > 0 { + zb0024-- + (*z).AccountData.TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0024 > 0 { + err = msgp.ErrTooManyArrayFields(zb0024) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + if zb0025 { + (*z).AccountData.TotalAppSchema = StateSchema{} + } + for zb0024 > 0 { + zb0024-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + switch string(field) { + case "nui": + (*z).AccountData.TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "NumUint") + return + } + case "nbs": + (*z).AccountData.TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAppSchema") + return + } + } + } + } + } + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0010 { + (*z) = BalanceRecord{} + } + for zb0009 > 0 { + zb0009-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "addr": + bts, err = (*z).Addr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Addr") + return + } + case "onl": + { + var zb0026 byte + zb0026, bts, err = msgp.ReadByteBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Status") + return + } + (*z).AccountData.Status = Status(zb0026) + } + case "algo": + bts, err = (*z).AccountData.MicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "MicroAlgos") + return + } + case "ebase": + (*z).AccountData.RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "RewardsBase") + return + } + case "ern": + bts, err = (*z).AccountData.RewardedMicroAlgos.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "RewardedMicroAlgos") + return + } + case "vote": + bts, err = (*z).AccountData.VoteID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "VoteID") + return + } + case "sel": + bts, err = (*z).AccountData.SelectionID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "SelectionID") + return + } + case "voteFst": + { + var zb0027 uint64 + zb0027, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteFirstValid") + return + } + (*z).AccountData.VoteFirstValid = Round(zb0027) + } + case "voteLst": + { + var zb0028 uint64 + zb0028, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteLastValid") + return + } + (*z).AccountData.VoteLastValid = Round(zb0028) + } + case "voteKD": + (*z).AccountData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "VoteKeyDilution") + return + } + case "apar": + var zb0029 int + var zb0030 bool + zb0029, zb0030, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AssetParams") + return + } + if zb0029 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0029), uint64(encodedMaxAssetsPerAccount)) + err = msgp.WrapError(err, "AssetParams") + return + } + if zb0030 { + (*z).AccountData.AssetParams = nil + } else if (*z).AccountData.AssetParams == nil { + (*z).AccountData.AssetParams = make(map[AssetIndex]AssetParams, zb0029) + } + for zb0029 > 0 { + var zb0001 AssetIndex + var zb0002 AssetParams + zb0029-- + bts, err = zb0001.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AssetParams") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AssetParams", zb0001) + return + } + (*z).AccountData.AssetParams[zb0001] = zb0002 + } + case "asset": + var zb0031 int + var zb0032 bool + zb0031, zb0032, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets") + return + } + if zb0031 > encodedMaxAssetsPerAccount { + err = msgp.ErrOverflow(uint64(zb0031), uint64(encodedMaxAssetsPerAccount)) + err = msgp.WrapError(err, "Assets") + return + } + if zb0032 { + (*z).AccountData.Assets = nil + } else if (*z).AccountData.Assets == nil { + (*z).AccountData.Assets = make(map[AssetIndex]AssetHolding, zb0031) + } + for zb0031 > 0 { + var zb0003 AssetIndex + var zb0004 AssetHolding + zb0031-- + bts, err = zb0003.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Assets") + return + } + var zb0033 int + var zb0034 bool + zb0033, zb0034, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0033, zb0034, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003) + return + } + if zb0033 > 0 { + zb0033-- + zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Amount") + return + } + } + if zb0033 > 0 { + zb0033-- + zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Frozen") + return + } + } + if zb0033 > 0 { + err = msgp.ErrTooManyArrayFields(zb0033) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003) + return + } + if zb0034 { + zb0004 = AssetHolding{} + } + for zb0033 > 0 { + zb0033-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003) + return + } + switch string(field) { + case "a": + zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003, "Amount") + return + } + case "f": + zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003, "Frozen") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "Assets", zb0003) + return + } + } + } + } + (*z).AccountData.Assets[zb0003] = zb0004 + } + case "spend": + bts, err = (*z).AccountData.AuthAddr.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AuthAddr") + return + } + case "appl": + var zb0035 int + var zb0036 bool + zb0035, zb0036, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates") + return + } + if zb0035 > encodedMaxAppLocalStates { + err = msgp.ErrOverflow(uint64(zb0035), uint64(encodedMaxAppLocalStates)) + err = msgp.WrapError(err, "AppLocalStates") + return + } + if zb0036 { + (*z).AccountData.AppLocalStates = nil + } else if (*z).AccountData.AppLocalStates == nil { + (*z).AccountData.AppLocalStates = make(map[AppIndex]AppLocalState, zb0035) + } + for zb0035 > 0 { + var zb0005 AppIndex + var zb0006 AppLocalState + zb0035-- + bts, err = zb0005.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates") + return + } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppLocalStates", zb0005) + return + } + (*z).AccountData.AppLocalStates[zb0005] = zb0006 + } + case "appp": + var zb0037 int + var zb0038 bool + zb0037, zb0038, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams") + return + } + if zb0037 > encodedMaxAppParams { + err = msgp.ErrOverflow(uint64(zb0037), uint64(encodedMaxAppParams)) + err = msgp.WrapError(err, "AppParams") + return + } + if zb0038 { + (*z).AccountData.AppParams = nil + } else if (*z).AccountData.AppParams == nil { + (*z).AccountData.AppParams = make(map[AppIndex]AppParams, zb0037) + } + for zb0037 > 0 { + var zb0007 AppIndex + var zb0008 AppParams + zb0037-- + bts, err = zb0007.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams") + return + } + bts, err = zb0008.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "AppParams", zb0007) + return + } + (*z).AccountData.AppParams[zb0007] = zb0008 + } + case "tsch": + var zb0039 int + var zb0040 bool + zb0039, zb0040, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0039, zb0040, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + if zb0039 > 0 { + zb0039-- + (*z).AccountData.TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array", "NumUint") + return + } + } + if zb0039 > 0 { + zb0039-- + (*z).AccountData.TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0039 > 0 { + err = msgp.ErrTooManyArrayFields(zb0039) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + if zb0040 { + (*z).AccountData.TotalAppSchema = StateSchema{} + } + for zb0039 > 0 { + zb0039-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } + switch string(field) { + case "nui": + (*z).AccountData.TotalAppSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "NumUint") + return + } + case "nbs": + (*z).AccountData.TotalAppSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "TotalAppSchema") + return + } } } - } else { + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *BalanceRecord) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*BalanceRecord) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *BalanceRecord) Msgsize() (s int) { + s = 3 + 5 + (*z).Addr.Msgsize() + 4 + msgp.ByteSize + 5 + (*z).AccountData.MicroAlgos.Msgsize() + 6 + msgp.Uint64Size + 4 + (*z).AccountData.RewardedMicroAlgos.Msgsize() + 5 + (*z).AccountData.VoteID.Msgsize() + 4 + (*z).AccountData.SelectionID.Msgsize() + 8 + msgp.Uint64Size + 8 + msgp.Uint64Size + 7 + msgp.Uint64Size + 5 + msgp.MapHeaderSize + if (*z).AccountData.AssetParams != nil { + for zb0001, zb0002 := range (*z).AccountData.AssetParams { + _ = zb0001 + _ = zb0002 + s += 0 + zb0001.Msgsize() + zb0002.Msgsize() + } + } + s += 6 + msgp.MapHeaderSize + if (*z).AccountData.Assets != nil { + for zb0003, zb0004 := range (*z).AccountData.Assets { + _ = zb0003 + _ = zb0004 + s += 0 + zb0003.Msgsize() + 1 + 2 + msgp.Uint64Size + 2 + msgp.BoolSize + } + } + s += 6 + (*z).AccountData.AuthAddr.Msgsize() + 5 + msgp.MapHeaderSize + if (*z).AccountData.AppLocalStates != nil { + for zb0005, zb0006 := range (*z).AccountData.AppLocalStates { + _ = zb0005 + _ = zb0006 + s += 0 + zb0005.Msgsize() + zb0006.Msgsize() + } + } + s += 5 + msgp.MapHeaderSize + if (*z).AccountData.AppParams != nil { + for zb0007, zb0008 := range (*z).AccountData.AppParams { + _ = zb0007 + _ = zb0008 + s += 0 + zb0007.Msgsize() + zb0008.Msgsize() + } + } + s += 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *BalanceRecord) MsgIsZero() bool { + return ((*z).Addr.MsgIsZero()) && ((*z).AccountData.Status == 0) && ((*z).AccountData.MicroAlgos.MsgIsZero()) && ((*z).AccountData.RewardsBase == 0) && ((*z).AccountData.RewardedMicroAlgos.MsgIsZero()) && ((*z).AccountData.VoteID.MsgIsZero()) && ((*z).AccountData.SelectionID.MsgIsZero()) && ((*z).AccountData.VoteFirstValid == 0) && ((*z).AccountData.VoteLastValid == 0) && ((*z).AccountData.VoteKeyDilution == 0) && (len((*z).AccountData.AssetParams) == 0) && (len((*z).AccountData.Assets) == 0) && ((*z).AccountData.AuthAddr.MsgIsZero()) && (len((*z).AccountData.AppLocalStates) == 0) && (len((*z).AccountData.AppParams) == 0) && (((*z).AccountData.TotalAppSchema.NumUint == 0) && ((*z).AccountData.TotalAppSchema.NumByteSlice == 0)) +} + +// MarshalMsg implements msgp.Marshaler +func (z CreatableIndex) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ CreatableIndex) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(CreatableIndex) + if !ok { + _, ok = (z).(*CreatableIndex) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *CreatableIndex) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = CreatableIndex(zb0001) + } + o = bts + return +} + +func (_ *CreatableIndex) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*CreatableIndex) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z CreatableIndex) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z CreatableIndex) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z CreatableType) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ CreatableType) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(CreatableType) + if !ok { + _, ok = (z).(*CreatableType) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *CreatableType) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = CreatableType(zb0001) + } + o = bts + return +} + +func (_ *CreatableType) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*CreatableType) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z CreatableType) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z CreatableType) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z DeltaAction) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ DeltaAction) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(DeltaAction) + if !ok { + _, ok = (z).(*DeltaAction) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *DeltaAction) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = DeltaAction(zb0001) + } + o = bts + return +} + +func (_ *DeltaAction) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*DeltaAction) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z DeltaAction) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z DeltaAction) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *EvalDelta) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0007Len := uint32(2) + var zb0007Mask uint8 /* 3 bits */ + if len((*z).GlobalDelta) == 0 { + zb0007Len-- + zb0007Mask |= 0x2 + } + if len((*z).LocalDeltas) == 0 { + zb0007Len-- + zb0007Mask |= 0x4 + } + // variable map header, size zb0007Len + o = append(o, 0x80|uint8(zb0007Len)) + if zb0007Len != 0 { + if (zb0007Mask & 0x2) == 0 { // if not empty + // string "gd" + o = append(o, 0xa2, 0x67, 0x64) + if (*z).GlobalDelta == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).GlobalDelta))) + } + zb0001_keys := make([]string, 0, len((*z).GlobalDelta)) + for zb0001 := range (*z).GlobalDelta { + zb0001_keys = append(zb0001_keys, zb0001) + } + sort.Sort(SortString(zb0001_keys)) + for _, zb0001 := range zb0001_keys { + zb0002 := (*z).GlobalDelta[zb0001] + _ = zb0002 + o = msgp.AppendString(o, zb0001) + o, err = zb0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "GlobalDelta", zb0001) + return + } + } + } + if (zb0007Mask & 0x4) == 0 { // if not empty + // string "ld" + o = append(o, 0xa2, 0x6c, 0x64) + if (*z).LocalDeltas == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len((*z).LocalDeltas))) + } + zb0003_keys := make([]uint64, 0, len((*z).LocalDeltas)) + for zb0003 := range (*z).LocalDeltas { + zb0003_keys = append(zb0003_keys, zb0003) + } + sort.Sort(SortUint64(zb0003_keys)) + for _, zb0003 := range zb0003_keys { + zb0004 := (*z).LocalDeltas[zb0003] + _ = zb0004 + o = msgp.AppendUint64(o, zb0003) + if zb0004 == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len(zb0004))) + } + zb0005_keys := make([]string, 0, len(zb0004)) + for zb0005 := range zb0004 { + zb0005_keys = append(zb0005_keys, zb0005) + } + sort.Sort(SortString(zb0005_keys)) + for _, zb0005 := range zb0005_keys { + zb0006 := zb0004[zb0005] + _ = zb0006 + o = msgp.AppendString(o, zb0005) + o, err = zb0006.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "LocalDeltas", zb0003, zb0005) + return + } + } + } + } + } + return +} + +func (_ *EvalDelta) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*EvalDelta) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *EvalDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0007 > 0 { + zb0007-- + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") + return + } + if zb0009 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0009), uint64(config.MaxStateDeltaKeys)) + err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") + return + } + if zb0010 { + (*z).GlobalDelta = nil + } else if (*z).GlobalDelta == nil { + (*z).GlobalDelta = make(StateDelta, zb0009) + } + for zb0009 > 0 { + var zb0001 string + var zb0002 ValueDelta + zb0009-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalDelta") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalDelta", zb0001) + return + } + (*z).GlobalDelta[zb0001] = zb0002 + } + } + if zb0007 > 0 { + zb0007-- + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") + return + } + if zb0011 > config.MaxEvalDeltaAccounts { + err = msgp.ErrOverflow(uint64(zb0011), uint64(config.MaxEvalDeltaAccounts)) + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") + return + } + if zb0012 { + (*z).LocalDeltas = nil + } else if (*z).LocalDeltas == nil { + (*z).LocalDeltas = make(map[uint64]StateDelta, zb0011) + } + for zb0011 > 0 { + var zb0003 uint64 + var zb0004 StateDelta + zb0011-- + zb0003, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas") + return + } + var zb0013 int + var zb0014 bool + zb0013, zb0014, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) + return + } + if zb0013 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0013), uint64(config.MaxStateDeltaKeys)) + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) + return + } + if zb0014 { + zb0004 = nil + } else if zb0004 == nil { + zb0004 = make(StateDelta, zb0013) + } + for zb0013 > 0 { + var zb0005 string + var zb0006 ValueDelta + zb0013-- + zb0005, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003) + return + } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalDeltas", zb0003, zb0005) + return + } + zb0004[zb0005] = zb0006 + } + (*z).LocalDeltas[zb0003] = zb0004 + } + } + if zb0007 > 0 { + err = msgp.ErrTooManyArrayFields(zb0007) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0008 { + (*z) = EvalDelta{} + } + for zb0007 > 0 { + zb0007-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "gd": + var zb0015 int + var zb0016 bool + zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalDelta") + return + } + if zb0015 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxStateDeltaKeys)) + err = msgp.WrapError(err, "GlobalDelta") + return + } + if zb0016 { + (*z).GlobalDelta = nil + } else if (*z).GlobalDelta == nil { + (*z).GlobalDelta = make(StateDelta, zb0015) + } + for zb0015 > 0 { + var zb0001 string + var zb0002 ValueDelta + zb0015-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalDelta") + return + } + bts, err = zb0002.UnmarshalMsg(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + err = msgp.WrapError(err, "GlobalDelta", zb0001) return } - if zb0015 { - zb0004 = AssetHolding{} + (*z).GlobalDelta[zb0001] = zb0002 + } + case "ld": + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalDeltas") + return + } + if zb0017 > config.MaxEvalDeltaAccounts { + err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxEvalDeltaAccounts)) + err = msgp.WrapError(err, "LocalDeltas") + return + } + if zb0018 { + (*z).LocalDeltas = nil + } else if (*z).LocalDeltas == nil { + (*z).LocalDeltas = make(map[uint64]StateDelta, zb0017) + } + for zb0017 > 0 { + var zb0003 uint64 + var zb0004 StateDelta + zb0017-- + zb0003, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalDeltas") + return } - for zb0014 > 0 { - zb0014-- - field, bts, err = msgp.ReadMapKeyZC(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalDeltas", zb0003) + return + } + if zb0019 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0019), uint64(config.MaxStateDeltaKeys)) + err = msgp.WrapError(err, "LocalDeltas", zb0003) + return + } + if zb0020 { + zb0004 = nil + } else if zb0004 == nil { + zb0004 = make(StateDelta, zb0019) + } + for zb0019 > 0 { + var zb0005 string + var zb0006 ValueDelta + zb0019-- + zb0005, bts, err = msgp.ReadStringBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) + err = msgp.WrapError(err, "LocalDeltas", zb0003) return } - switch string(field) { - case "a": - zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "Amount") - return - } - case "f": - zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003, "Frozen") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Assets", zb0003) - return - } + bts, err = zb0006.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "LocalDeltas", zb0003, zb0005) + return } + zb0004[zb0005] = zb0006 } + (*z).LocalDeltas[zb0003] = zb0004 } - (*z).AccountData.Assets[zb0003] = zb0004 + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *EvalDelta) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*EvalDelta) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *EvalDelta) Msgsize() (s int) { + s = 1 + 3 + msgp.MapHeaderSize + if (*z).GlobalDelta != nil { + for zb0001, zb0002 := range (*z).GlobalDelta { + _ = zb0001 + _ = zb0002 + s += 0 + msgp.StringPrefixSize + len(zb0001) + zb0002.Msgsize() + } + } + s += 3 + msgp.MapHeaderSize + if (*z).LocalDeltas != nil { + for zb0003, zb0004 := range (*z).LocalDeltas { + _ = zb0003 + _ = zb0004 + s += 0 + msgp.Uint64Size + msgp.MapHeaderSize + if zb0004 != nil { + for zb0005, zb0006 := range zb0004 { + _ = zb0005 + _ = zb0006 + s += 0 + msgp.StringPrefixSize + len(zb0005) + zb0006.Msgsize() + } + } + } + } + return +} + +// MsgIsZero returns whether this is a zero value +func (z *EvalDelta) MsgIsZero() bool { + return (len((*z).GlobalDelta) == 0) && (len((*z).LocalDeltas) == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z Round) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ Round) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(Round) + if !ok { + _, ok = (z).(*Round) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *Round) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = Round(zb0001) + } + o = bts + return +} + +func (_ *Round) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*Round) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z Round) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z Round) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z RoundInterval) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ RoundInterval) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(RoundInterval) + if !ok { + _, ok = (z).(*RoundInterval) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *RoundInterval) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = RoundInterval(zb0001) + } + o = bts + return +} + +func (_ *RoundInterval) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*RoundInterval) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z RoundInterval) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z RoundInterval) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z StateDelta) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + if z == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len(z))) + } + za0001_keys := make([]string, 0, len(z)) + for za0001 := range z { + za0001_keys = append(za0001_keys, za0001) + } + sort.Sort(SortString(za0001_keys)) + for _, za0001 := range za0001_keys { + za0002 := z[za0001] + _ = za0002 + o = msgp.AppendString(o, za0001) + o, err = za0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, za0001) + return + } + } + return +} + +func (_ StateDelta) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(StateDelta) + if !ok { + _, ok = (z).(*StateDelta) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *StateDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 > config.MaxStateDeltaKeys { + err = msgp.ErrOverflow(uint64(zb0003), uint64(config.MaxStateDeltaKeys)) + err = msgp.WrapError(err) + return + } + if zb0004 { + (*z) = nil + } else if (*z) == nil { + (*z) = make(StateDelta, zb0003) + } + for zb0003 > 0 { + var zb0001 string + var zb0002 ValueDelta + zb0003-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + (*z)[zb0001] = zb0002 + } + o = bts + return +} + +func (_ *StateDelta) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*StateDelta) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z StateDelta) Msgsize() (s int) { + s = msgp.MapHeaderSize + if z != nil { + for za0001, za0002 := range z { + _ = za0001 + _ = za0002 + s += 0 + msgp.StringPrefixSize + len(za0001) + za0002.Msgsize() + } + } + return +} + +// MsgIsZero returns whether this is a zero value +func (z StateDelta) MsgIsZero() bool { + return len(z) == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *StateSchema) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if (*z).NumByteSlice == 0 { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).NumUint == 0 { + zb0001Len-- + zb0001Mask |= 0x4 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).NumByteSlice) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).NumUint) + } + } + return +} + +func (_ *StateSchema) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*StateSchema) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *StateSchema) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 > 0 { + zb0001-- + (*z).NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "NumUint") + return } } - if zb0005 > 0 { - zb0005-- - bts, err = (*z).AccountData.AuthAddr.UnmarshalMsg(bts) + if zb0001 > 0 { + zb0001-- + (*z).NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "AuthAddr") + err = msgp.WrapError(err, "struct-from-array", "NumByteSlice") return } } - if zb0005 > 0 { - err = msgp.ErrTooManyArrayFields(zb0005) + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -1849,225 +4768,27 @@ func (z *BalanceRecord) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0006 { - (*z) = BalanceRecord{} + if zb0002 { + (*z) = StateSchema{} } - for zb0005 > 0 { - zb0005-- + for zb0001 > 0 { + zb0001-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) return } switch string(field) { - case "addr": - bts, err = (*z).Addr.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Addr") - return - } - case "onl": - { - var zb0016 byte - zb0016, bts, err = msgp.ReadByteBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Status") - return - } - (*z).AccountData.Status = Status(zb0016) - } - case "algo": - bts, err = (*z).AccountData.MicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "MicroAlgos") - return - } - case "ebase": - (*z).AccountData.RewardsBase, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "RewardsBase") - return - } - case "ern": - bts, err = (*z).AccountData.RewardedMicroAlgos.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "RewardedMicroAlgos") - return - } - case "vote": - bts, err = (*z).AccountData.VoteID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "VoteID") - return - } - case "sel": - bts, err = (*z).AccountData.SelectionID.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "SelectionID") - return - } - case "voteFst": - { - var zb0017 uint64 - zb0017, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "VoteFirstValid") - return - } - (*z).AccountData.VoteFirstValid = Round(zb0017) - } - case "voteLst": - { - var zb0018 uint64 - zb0018, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "VoteLastValid") - return - } - (*z).AccountData.VoteLastValid = Round(zb0018) - } - case "voteKD": - (*z).AccountData.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "VoteKeyDilution") - return - } - case "apar": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "AssetParams") - return - } - if zb0019 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxAssetsPerAccount)) - err = msgp.WrapError(err, "AssetParams") - return - } - if zb0020 { - (*z).AccountData.AssetParams = nil - } else if (*z).AccountData.AssetParams == nil { - (*z).AccountData.AssetParams = make(map[AssetIndex]AssetParams, zb0019) - } - for zb0019 > 0 { - var zb0001 AssetIndex - var zb0002 AssetParams - zb0019-- - bts, err = zb0001.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "AssetParams") - return - } - bts, err = zb0002.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "AssetParams", zb0001) - return - } - (*z).AccountData.AssetParams[zb0001] = zb0002 - } - case "asset": - var zb0021 int - var zb0022 bool - zb0021, zb0022, bts, err = msgp.ReadMapHeaderBytes(bts) + case "nui": + (*z).NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "Assets") - return - } - if zb0021 > encodedMaxAssetsPerAccount { - err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxAssetsPerAccount)) - err = msgp.WrapError(err, "Assets") + err = msgp.WrapError(err, "NumUint") return } - if zb0022 { - (*z).AccountData.Assets = nil - } else if (*z).AccountData.Assets == nil { - (*z).AccountData.Assets = make(map[AssetIndex]AssetHolding, zb0021) - } - for zb0021 > 0 { - var zb0003 AssetIndex - var zb0004 AssetHolding - zb0021-- - bts, err = zb0003.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Assets") - return - } - var zb0023 int - var zb0024 bool - zb0023, zb0024, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0023, zb0024, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003) - return - } - if zb0023 > 0 { - zb0023-- - zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Amount") - return - } - } - if zb0023 > 0 { - zb0023-- - zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array", "Frozen") - return - } - } - if zb0023 > 0 { - err = msgp.ErrTooManyArrayFields(zb0023) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003) - return - } - if zb0024 { - zb0004 = AssetHolding{} - } - for zb0023 > 0 { - zb0023-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003) - return - } - switch string(field) { - case "a": - zb0004.Amount, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003, "Amount") - return - } - case "f": - zb0004.Frozen, bts, err = msgp.ReadBoolBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003, "Frozen") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "Assets", zb0003) - return - } - } - } - } - (*z).AccountData.Assets[zb0003] = zb0004 - } - case "spend": - bts, err = (*z).AccountData.AuthAddr.UnmarshalMsg(bts) + case "nbs": + (*z).NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "AuthAddr") + err = msgp.WrapError(err, "NumByteSlice") return } default: @@ -2079,59 +4800,185 @@ func (z *BalanceRecord) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - o = bts + o = bts + return +} + +func (_ *StateSchema) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*StateSchema) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *StateSchema) Msgsize() (s int) { + s = 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *StateSchema) MsgIsZero() bool { + return ((*z).NumUint == 0) && ((*z).NumByteSlice == 0) +} + +// MarshalMsg implements msgp.Marshaler +func (z Status) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + o = msgp.AppendByte(o, byte(z)) + return +} + +func (_ Status) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(Status) + if !ok { + _, ok = (z).(*Status) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *Status) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 byte + zb0001, bts, err = msgp.ReadByteBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = Status(zb0001) + } + o = bts + return +} + +func (_ *Status) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*Status) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z Status) Msgsize() (s int) { + s = msgp.ByteSize + return +} + +// MsgIsZero returns whether this is a zero value +func (z Status) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z TealKeyValue) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + if z == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendMapHeader(o, uint32(len(z))) + } + za0001_keys := make([]string, 0, len(z)) + for za0001 := range z { + za0001_keys = append(za0001_keys, za0001) + } + sort.Sort(SortString(za0001_keys)) + for _, za0001 := range za0001_keys { + za0002 := z[za0001] + _ = za0002 + o = msgp.AppendString(o, za0001) + o, err = za0002.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, za0001) + return + } + } return } -func (_ *BalanceRecord) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*BalanceRecord) +func (_ TealKeyValue) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(TealKeyValue) + if !ok { + _, ok = (z).(*TealKeyValue) + } return ok } -// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *BalanceRecord) Msgsize() (s int) { - s = 1 + 5 + (*z).Addr.Msgsize() + 4 + msgp.ByteSize + 5 + (*z).AccountData.MicroAlgos.Msgsize() + 6 + msgp.Uint64Size + 4 + (*z).AccountData.RewardedMicroAlgos.Msgsize() + 5 + (*z).AccountData.VoteID.Msgsize() + 4 + (*z).AccountData.SelectionID.Msgsize() + 8 + msgp.Uint64Size + 8 + msgp.Uint64Size + 7 + msgp.Uint64Size + 5 + msgp.MapHeaderSize - if (*z).AccountData.AssetParams != nil { - for zb0001, zb0002 := range (*z).AccountData.AssetParams { - _ = zb0001 - _ = zb0002 - s += 0 + zb0001.Msgsize() + zb0002.Msgsize() +// UnmarshalMsg implements msgp.Unmarshaler +func (z *TealKeyValue) UnmarshalMsg(bts []byte) (o []byte, err error) { + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0003 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0003), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err) + return + } + if zb0004 { + (*z) = nil + } else if (*z) == nil { + (*z) = make(TealKeyValue, zb0003) + } + for zb0003 > 0 { + var zb0001 string + var zb0002 TealValue + zb0003-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, zb0001) + return + } + (*z)[zb0001] = zb0002 } - s += 6 + msgp.MapHeaderSize - if (*z).AccountData.Assets != nil { - for zb0003, zb0004 := range (*z).AccountData.Assets { - _ = zb0003 - _ = zb0004 - s += 0 + zb0003.Msgsize() + 1 + 2 + msgp.Uint64Size + 2 + msgp.BoolSize + o = bts + return +} + +func (_ *TealKeyValue) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*TealKeyValue) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z TealKeyValue) Msgsize() (s int) { + s = msgp.MapHeaderSize + if z != nil { + for za0001, za0002 := range z { + _ = za0001 + _ = za0002 + s += 0 + msgp.StringPrefixSize + len(za0001) + za0002.Msgsize() } } - s += 6 + (*z).AccountData.AuthAddr.Msgsize() return } // MsgIsZero returns whether this is a zero value -func (z *BalanceRecord) MsgIsZero() bool { - return ((*z).Addr.MsgIsZero()) && ((*z).AccountData.Status == 0) && ((*z).AccountData.MicroAlgos.MsgIsZero()) && ((*z).AccountData.RewardsBase == 0) && ((*z).AccountData.RewardedMicroAlgos.MsgIsZero()) && ((*z).AccountData.VoteID.MsgIsZero()) && ((*z).AccountData.SelectionID.MsgIsZero()) && ((*z).AccountData.VoteFirstValid == 0) && ((*z).AccountData.VoteLastValid == 0) && ((*z).AccountData.VoteKeyDilution == 0) && (len((*z).AccountData.AssetParams) == 0) && (len((*z).AccountData.Assets) == 0) && ((*z).AccountData.AuthAddr.MsgIsZero()) +func (z TealKeyValue) MsgIsZero() bool { + return len(z) == 0 } // MarshalMsg implements msgp.Marshaler -func (z Round) MarshalMsg(b []byte) (o []byte, err error) { +func (z TealType) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) o = msgp.AppendUint64(o, uint64(z)) return } -func (_ Round) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(Round) +func (_ TealType) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(TealType) if !ok { - _, ok = (z).(*Round) + _, ok = (z).(*TealType) } return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *Round) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *TealType) UnmarshalMsg(bts []byte) (o []byte, err error) { { var zb0001 uint64 zb0001, bts, err = msgp.ReadUint64Bytes(bts) @@ -2139,116 +4986,344 @@ func (z *Round) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - (*z) = Round(zb0001) + (*z) = TealType(zb0001) } o = bts return } -func (_ *Round) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*Round) +func (_ *TealType) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*TealType) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z Round) Msgsize() (s int) { +func (z TealType) Msgsize() (s int) { s = msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value -func (z Round) MsgIsZero() bool { +func (z TealType) MsgIsZero() bool { return z == 0 } // MarshalMsg implements msgp.Marshaler -func (z RoundInterval) MarshalMsg(b []byte) (o []byte, err error) { +func (z *TealValue) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendUint64(o, uint64(z)) + // omitempty: check for empty values + zb0001Len := uint32(3) + var zb0001Mask uint8 /* 4 bits */ + if (*z).Bytes == "" { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Type == 0 { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).Uint == 0 { + zb0001Len-- + zb0001Mask |= 0x8 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "tb" + o = append(o, 0xa2, 0x74, 0x62) + o = msgp.AppendString(o, (*z).Bytes) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "tt" + o = append(o, 0xa2, 0x74, 0x74) + o = msgp.AppendUint64(o, uint64((*z).Type)) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "ui" + o = append(o, 0xa2, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).Uint) + } + } return } -func (_ RoundInterval) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(RoundInterval) - if !ok { - _, ok = (z).(*RoundInterval) - } +func (_ *TealValue) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*TealValue) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *RoundInterval) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 uint64 - zb0001, bts, err = msgp.ReadUint64Bytes(bts) +func (z *TealValue) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - (*z) = RoundInterval(zb0001) + if zb0001 > 0 { + zb0001-- + { + var zb0003 uint64 + zb0003, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Type") + return + } + (*z).Type = TealType(zb0003) + } + } + if zb0001 > 0 { + zb0001-- + (*z).Bytes, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Bytes") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).Uint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Uint") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = TealValue{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "tt": + { + var zb0004 uint64 + zb0004, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Type") + return + } + (*z).Type = TealType(zb0004) + } + case "tb": + (*z).Bytes, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Bytes") + return + } + case "ui": + (*z).Uint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Uint") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } } o = bts return } -func (_ *RoundInterval) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*RoundInterval) +func (_ *TealValue) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*TealValue) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z RoundInterval) Msgsize() (s int) { - s = msgp.Uint64Size +func (z *TealValue) Msgsize() (s int) { + s = 1 + 3 + msgp.Uint64Size + 3 + msgp.StringPrefixSize + len((*z).Bytes) + 3 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value -func (z RoundInterval) MsgIsZero() bool { - return z == 0 +func (z *TealValue) MsgIsZero() bool { + return ((*z).Type == 0) && ((*z).Bytes == "") && ((*z).Uint == 0) } // MarshalMsg implements msgp.Marshaler -func (z Status) MarshalMsg(b []byte) (o []byte, err error) { +func (z *ValueDelta) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - o = msgp.AppendByte(o, byte(z)) + // omitempty: check for empty values + zb0001Len := uint32(3) + var zb0001Mask uint8 /* 4 bits */ + if (*z).Action == 0 { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).Bytes == "" { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).Uint == 0 { + zb0001Len-- + zb0001Mask |= 0x8 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "at" + o = append(o, 0xa2, 0x61, 0x74) + o = msgp.AppendUint64(o, uint64((*z).Action)) + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "bs" + o = append(o, 0xa2, 0x62, 0x73) + o = msgp.AppendString(o, (*z).Bytes) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "ui" + o = append(o, 0xa2, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).Uint) + } + } return } -func (_ Status) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(Status) - if !ok { - _, ok = (z).(*Status) - } +func (_ *ValueDelta) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*ValueDelta) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *Status) UnmarshalMsg(bts []byte) (o []byte, err error) { - { - var zb0001 byte - zb0001, bts, err = msgp.ReadByteBytes(bts) +func (z *ValueDelta) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - (*z) = Status(zb0001) + if zb0001 > 0 { + zb0001-- + { + var zb0003 uint64 + zb0003, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Action") + return + } + (*z).Action = DeltaAction(zb0003) + } + } + if zb0001 > 0 { + zb0001-- + (*z).Bytes, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Bytes") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).Uint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Uint") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = ValueDelta{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "at": + { + var zb0004 uint64 + zb0004, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Action") + return + } + (*z).Action = DeltaAction(zb0004) + } + case "bs": + (*z).Bytes, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Bytes") + return + } + case "ui": + (*z).Uint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "Uint") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } } o = bts return } -func (_ *Status) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*Status) +func (_ *ValueDelta) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*ValueDelta) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z Status) Msgsize() (s int) { - s = msgp.ByteSize +func (z *ValueDelta) Msgsize() (s int) { + s = 1 + 3 + msgp.Uint64Size + 3 + msgp.StringPrefixSize + len((*z).Bytes) + 3 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value -func (z Status) MsgIsZero() bool { - return z == 0 +func (z *ValueDelta) MsgIsZero() bool { + return ((*z).Action == 0) && ((*z).Bytes == "") && ((*z).Uint == 0) } diff --git a/data/basics/msgp_gen_test.go b/data/basics/msgp_gen_test.go index 5e706cfb78..64a4e7ce1d 100644 --- a/data/basics/msgp_gen_test.go +++ b/data/basics/msgp_gen_test.go @@ -73,6 +73,130 @@ func BenchmarkUnmarshalAccountData(b *testing.B) { } } +func TestMarshalUnmarshalAppLocalState(t *testing.T) { + v := AppLocalState{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingAppLocalState(t *testing.T) { + protocol.RunEncodingTest(t, &AppLocalState{}) +} + +func BenchmarkMarshalMsgAppLocalState(b *testing.B) { + v := AppLocalState{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgAppLocalState(b *testing.B) { + v := AppLocalState{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalAppLocalState(b *testing.B) { + v := AppLocalState{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalAppParams(t *testing.T) { + v := AppParams{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingAppParams(t *testing.T) { + protocol.RunEncodingTest(t, &AppParams{}) +} + +func BenchmarkMarshalMsgAppParams(b *testing.B) { + v := AppParams{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgAppParams(b *testing.B) { + v := AppParams{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalAppParams(b *testing.B) { + v := AppParams{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + func TestMarshalUnmarshalAssetHolding(t *testing.T) { v := AssetHolding{} bts, err := v.MarshalMsg(nil) @@ -258,3 +382,375 @@ func BenchmarkUnmarshalBalanceRecord(b *testing.B) { } } } + +func TestMarshalUnmarshalEvalDelta(t *testing.T) { + v := EvalDelta{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingEvalDelta(t *testing.T) { + protocol.RunEncodingTest(t, &EvalDelta{}) +} + +func BenchmarkMarshalMsgEvalDelta(b *testing.B) { + v := EvalDelta{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgEvalDelta(b *testing.B) { + v := EvalDelta{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalEvalDelta(b *testing.B) { + v := EvalDelta{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalStateDelta(t *testing.T) { + v := StateDelta{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingStateDelta(t *testing.T) { + protocol.RunEncodingTest(t, &StateDelta{}) +} + +func BenchmarkMarshalMsgStateDelta(b *testing.B) { + v := StateDelta{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgStateDelta(b *testing.B) { + v := StateDelta{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalStateDelta(b *testing.B) { + v := StateDelta{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalStateSchema(t *testing.T) { + v := StateSchema{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingStateSchema(t *testing.T) { + protocol.RunEncodingTest(t, &StateSchema{}) +} + +func BenchmarkMarshalMsgStateSchema(b *testing.B) { + v := StateSchema{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgStateSchema(b *testing.B) { + v := StateSchema{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalStateSchema(b *testing.B) { + v := StateSchema{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalTealKeyValue(t *testing.T) { + v := TealKeyValue{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingTealKeyValue(t *testing.T) { + protocol.RunEncodingTest(t, &TealKeyValue{}) +} + +func BenchmarkMarshalMsgTealKeyValue(b *testing.B) { + v := TealKeyValue{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgTealKeyValue(b *testing.B) { + v := TealKeyValue{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalTealKeyValue(b *testing.B) { + v := TealKeyValue{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalTealValue(t *testing.T) { + v := TealValue{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingTealValue(t *testing.T) { + protocol.RunEncodingTest(t, &TealValue{}) +} + +func BenchmarkMarshalMsgTealValue(b *testing.B) { + v := TealValue{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgTealValue(b *testing.B) { + v := TealValue{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalTealValue(b *testing.B) { + v := TealValue{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestMarshalUnmarshalValueDelta(t *testing.T) { + v := ValueDelta{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingValueDelta(t *testing.T) { + protocol.RunEncodingTest(t, &ValueDelta{}) +} + +func BenchmarkMarshalMsgValueDelta(b *testing.B) { + v := ValueDelta{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgValueDelta(b *testing.B) { + v := ValueDelta{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalValueDelta(b *testing.B) { + v := ValueDelta{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/data/basics/overflow.go b/data/basics/overflow.go index 2937b82d9d..3e28d17aa4 100644 --- a/data/basics/overflow.go +++ b/data/basics/overflow.go @@ -68,6 +68,24 @@ func MulSaturate(a uint64, b uint64) uint64 { return res } +// AddSaturate adds 2 values with saturation on overflow +func AddSaturate(a uint64, b uint64) uint64 { + res, overflowed := OAdd(a, b) + if overflowed { + return math.MaxUint64 + } + return res +} + +// SubSaturate subtracts 2 values with saturation on underflow +func SubSaturate(a uint64, b uint64) uint64 { + res, overflowed := OSub(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/sort.go b/data/basics/sort.go index cbceca5936..e440e96f87 100644 --- a/data/basics/sort.go +++ b/data/basics/sort.go @@ -16,6 +16,20 @@ package basics +import ( + "bytes" +) + +// SortUint64 implements sorting by uint64 keys for +// canonical encoding of maps in msgpack format. +//msgp:ignore SortUint64 +//msgp:sort uint64 SortUint64 +type SortUint64 []uint64 + +func (a SortUint64) Len() int { return len(a) } +func (a SortUint64) Less(i, j int) bool { return a[i] < a[j] } +func (a SortUint64) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + // SortAssetIndex implements sorting by AssetIndex keys for // canonical encoding of maps in msgpack format. //msgp:ignore SortAssetIndex @@ -25,3 +39,33 @@ type SortAssetIndex []AssetIndex func (a SortAssetIndex) Len() int { return len(a) } func (a SortAssetIndex) Less(i, j int) bool { return a[i] < a[j] } func (a SortAssetIndex) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// SortAppIndex implements sorting by AppIndex keys for +// canonical encoding of maps in msgpack format. +//msgp:ignore SortAppIndex +//msgp:sort AppIndex SortAppIndex +type SortAppIndex []AppIndex + +func (a SortAppIndex) Len() int { return len(a) } +func (a SortAppIndex) Less(i, j int) bool { return a[i] < a[j] } +func (a SortAppIndex) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// SortString implements sorting by string keys for +// canonical encoding of maps in msgpack format. +//msgp:ignore SortString +//msgp:sort string SortString +type SortString []string + +func (a SortString) Len() int { return len(a) } +func (a SortString) Less(i, j int) bool { return a[i] < a[j] } +func (a SortString) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// SortAddress implements sorting by Address keys for +// canonical encoding of maps in msgpack format. +//msgp:ignore SortAddress +//msgp:sort Address SortAddress +type SortAddress []Address + +func (a SortAddress) Len() int { return len(a) } +func (a SortAddress) Less(i, j int) bool { return bytes.Compare(a[i][:], a[j][:]) < 0 } +func (a SortAddress) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/data/basics/teal.go b/data/basics/teal.go new file mode 100644 index 0000000000..ed774c9c8a --- /dev/null +++ b/data/basics/teal.go @@ -0,0 +1,315 @@ +// 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 . + +package basics + +import ( + "encoding/hex" + "fmt" + + "github.com/algorand/go-algorand/config" +) + +// DeltaAction is an enum of actions that may be performed when applying a +// delta to a TEAL key/value store +type DeltaAction uint64 + +const ( + // SetUintAction indicates that a Uint should be stored at a key + SetUintAction DeltaAction = 1 + + // SetBytesAction indicates that a TEAL byte slice should be stored at a key + SetBytesAction DeltaAction = 2 + + // DeleteAction indicates that the value for a particular key should be deleted + DeleteAction DeltaAction = 3 +) + +// ValueDelta links a DeltaAction with a value to be set +type ValueDelta struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Action DeltaAction `codec:"at"` + Bytes string `codec:"bs"` + Uint uint64 `codec:"ui"` +} + +// ToTealValue converts a ValueDelta into a TealValue if possible, and returns +// ok = false with a value of 0 TealUint if the conversion is not possible. +func (vd *ValueDelta) ToTealValue() (value TealValue, ok bool) { + switch vd.Action { + case SetBytesAction: + value.Type = TealBytesType + value.Bytes = vd.Bytes + ok = true + case SetUintAction: + value.Type = TealUintType + value.Uint = vd.Uint + ok = true + case DeleteAction: + value.Type = TealUintType + ok = false + default: + value.Type = TealUintType + ok = false + } + return value, ok +} + +// StateDelta is a map from key/value store keys to ValueDeltas, indicating +// what should happen for that key +//msgp:allocbound StateDelta config.MaxStateDeltaKeys +type StateDelta map[string]ValueDelta + +// Equal checks whether two StateDeltas are equal. We don't check for nilness +// equality because an empty map will encode/decode as nil. So if our generated +// map is empty but not nil, we want to equal a decoded nil off the wire. +func (sd StateDelta) Equal(o StateDelta) bool { + // Lengths should be the same + if len(sd) != len(o) { + return false + } + // All keys and deltas should be the same + for k, v := range sd { + // Other StateDelta must contain key + ov, ok := o[k] + if !ok { + return false + } + + // Other StateDelta must have same value for key + if ov != v { + return false + } + } + return true +} + +// Valid checks whether the keys and values in a StateDelta conform to the +// consensus parameters' maximum lengths +func (sd StateDelta) Valid(proto *config.ConsensusParams) error { + if len(sd) > 0 && proto.MaxAppKeyLen == 0 { + return fmt.Errorf("delta not empty, but proto.MaxAppKeyLen is 0 (why did we make a delta?)") + } + for key, delta := range sd { + if len(key) > proto.MaxAppKeyLen { + return fmt.Errorf("key too long: length was %d, maximum is %d", len(key), proto.MaxAppKeyLen) + } + switch delta.Action { + case SetBytesAction: + if len(delta.Bytes) > proto.MaxAppBytesValueLen { + return fmt.Errorf("cannot set value for key 0x%x, too long: length was %d, maximum is %d", key, len(delta.Bytes), proto.MaxAppBytesValueLen) + } + case SetUintAction: + case DeleteAction: + default: + return fmt.Errorf("unknown delta action: %v", delta.Action) + } + } + return nil +} + +// EvalDelta stores StateDeltas for an application's global key/value store, as +// well as StateDeltas for some number of accounts holding local state for that +// application +type EvalDelta struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + GlobalDelta StateDelta `codec:"gd"` + + // When decoding EvalDeltas, the integer key represents an offset into + // [txn.Sender, txn.Accounts[0], txn.Accounts[1], ...] + LocalDeltas map[uint64]StateDelta `codec:"ld,allocbound=config.MaxEvalDeltaAccounts"` +} + +// Equal compares two EvalDeltas and returns whether or not they are +// equivalent. It does not care about nilness equality of LocalDeltas, +// because the msgpack codec will encode/decode an empty map as nil, and we want +// an empty generated EvalDelta to equal an empty one we decode off the wire. +func (ed EvalDelta) Equal(o EvalDelta) bool { + // LocalDeltas length should be the same + if len(ed.LocalDeltas) != len(o.LocalDeltas) { + return false + } + + // All keys and local StateDeltas should be the same + for k, v := range ed.LocalDeltas { + // Other LocalDelta must have value for key + ov, ok := o.LocalDeltas[k] + if !ok { + return false + } + + // Other LocalDelta must have same value for key + if !ov.Equal(v) { + return false + } + } + + // GlobalDeltas must be equal + if !ed.GlobalDelta.Equal(o.GlobalDelta) { + return false + } + + return true +} + +// StateSchema sets maximums on the number of each type that may be stored +type StateSchema struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + NumUint uint64 `codec:"nui"` + NumByteSlice uint64 `codec:"nbs"` +} + +// AddSchema adds two StateSchemas together +func (sm StateSchema) AddSchema(osm StateSchema) (out StateSchema) { + out.NumUint = AddSaturate(sm.NumUint, osm.NumUint) + out.NumByteSlice = AddSaturate(sm.NumByteSlice, osm.NumByteSlice) + return +} + +// SubSchema subtracts one StateSchema from another +func (sm StateSchema) SubSchema(osm StateSchema) (out StateSchema) { + out.NumUint = SubSaturate(sm.NumUint, osm.NumUint) + out.NumByteSlice = SubSaturate(sm.NumByteSlice, osm.NumByteSlice) + return +} + +// NumEntries counts the total number of values that may be stored for particular schema +func (sm StateSchema) NumEntries() (tot uint64) { + tot = AddSaturate(tot, sm.NumUint) + tot = AddSaturate(tot, sm.NumByteSlice) + return tot +} + +// MinBalance computes the MinBalance requirements for a StateSchema based on +// the consensus parameters +func (sm StateSchema) MinBalance(proto *config.ConsensusParams) (res MicroAlgos) { + // Flat cost for each key/value pair + flatCost := MulSaturate(proto.SchemaMinBalancePerEntry, sm.NumEntries()) + + // Cost for uints + uintCost := MulSaturate(proto.SchemaUintMinBalance, sm.NumUint) + + // Cost for byte slices + bytesCost := MulSaturate(proto.SchemaBytesMinBalance, sm.NumByteSlice) + + // Sum the separate costs + var min uint64 + min = AddSaturate(min, flatCost) + min = AddSaturate(min, uintCost) + min = AddSaturate(min, bytesCost) + + res.Raw = min + return res +} + +// TealType is an enum of the types in a TEAL program: Bytes and Uint +type TealType uint64 + +const ( + // TealBytesType represents the type of a byte slice in a TEAL program + TealBytesType TealType = 1 + + // TealUintType represents the type of a uint in a TEAL program + TealUintType TealType = 2 +) + +func (tt TealType) String() string { + switch tt { + case TealBytesType: + return "b" + case TealUintType: + return "u" + } + return "?" +} + +// TealValue contains type information and a value, representing a value in a +// TEAL program +type TealValue struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Type TealType `codec:"tt"` + Bytes string `codec:"tb"` + Uint uint64 `codec:"ui"` +} + +// ToValueDelta creates ValueDelta from TealValue +func (tv *TealValue) ToValueDelta() (vd ValueDelta) { + if tv.Type == TealUintType { + vd.Action = SetUintAction + vd.Uint = tv.Uint + } else { + vd.Action = SetBytesAction + vd.Bytes = tv.Bytes + } + return +} + +func (tv *TealValue) String() string { + if tv.Type == TealBytesType { + return hex.EncodeToString([]byte(tv.Bytes)) + } + return fmt.Sprintf("%d", tv.Uint) +} + +// TealKeyValue represents a key/value store for use in an application's +// LocalState or GlobalState +//msgp:allocbound TealKeyValue encodedMaxKeyValueEntries +type TealKeyValue map[string]TealValue + +// Clone returns a copy of a TealKeyValue that may be modified without +// affecting the original +func (tk TealKeyValue) Clone() TealKeyValue { + if tk == nil { + return nil + } + res := make(TealKeyValue, len(tk)) + for k, v := range tk { + res[k] = v + } + return res +} + +// SatisfiesSchema returns an error indicating whether or not a particular +// TealKeyValue store meets the requirements set by a StateSchema on how +// many values of each type are allowed +func (tk TealKeyValue) SatisfiesSchema(schema StateSchema) error { + // Count all of the types in the key/value store + var uintCount, bytesCount uint64 + for _, value := range tk { + switch value.Type { + case TealBytesType: + bytesCount++ + case TealUintType: + uintCount++ + default: + // Shouldn't happen + return fmt.Errorf("unknown type %v", value.Type) + } + } + + // Check against the schema + if uintCount > schema.NumUint { + return fmt.Errorf("store integer count %d exceeds schema integer count %d", uintCount, schema.NumUint) + } + if bytesCount > schema.NumByteSlice { + return fmt.Errorf("store bytes count %d exceeds schema bytes count %d", bytesCount, schema.NumByteSlice) + } + return nil +} diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go new file mode 100644 index 0000000000..e99db5b959 --- /dev/null +++ b/data/basics/teal_test.go @@ -0,0 +1,112 @@ +// 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 . + +package basics + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/protocol" +) + +func TestStateDeltaValid(t *testing.T) { + a := require.New(t) + + // test pre-applications proto + protoPreF := config.Consensus[protocol.ConsensusV23] + sd := StateDelta{"key": ValueDelta{Action: SetBytesAction, Bytes: "val"}} + err := sd.Valid(&protoPreF) + a.Error(err) + a.Contains(err.Error(), "proto.MaxAppKeyLen is 0") + + sd = StateDelta{"": ValueDelta{Action: SetUintAction, Uint: 1}} + err = sd.Valid(&protoPreF) + a.Error(err) + a.Contains(err.Error(), "proto.MaxAppKeyLen is 0") + + sd = StateDelta{"": ValueDelta{Action: SetBytesAction, Bytes: ""}} + err = sd.Valid(&protoPreF) + a.Error(err) + a.Contains(err.Error(), "proto.MaxAppKeyLen is 0") + + // test proto with applications + sd = StateDelta{"key": ValueDelta{Action: SetBytesAction, Bytes: "val"}} + protoF := config.Consensus[protocol.ConsensusFuture] + err = sd.Valid(&protoF) + a.NoError(err) + + tooLongKey := strings.Repeat("a", protoF.MaxAppKeyLen+1) + sd[tooLongKey] = ValueDelta{Action: SetBytesAction, Bytes: "val"} + err = sd.Valid(&protoF) + a.Error(err) + a.Contains(err.Error(), "key too long") + delete(sd, tooLongKey) + + longKey := tooLongKey[1:] + tooLongValue := strings.Repeat("b", protoF.MaxAppBytesValueLen+1) + sd[longKey] = ValueDelta{Action: SetBytesAction, Bytes: tooLongValue} + err = sd.Valid(&protoF) + a.Error(err) + a.Contains(err.Error(), "cannot set value for key") + + sd[longKey] = ValueDelta{Action: SetBytesAction, Bytes: tooLongValue[1:]} + sd["intval"] = ValueDelta{Action: DeltaAction(10), Uint: 0} + err = sd.Valid(&protoF) + a.Error(err) + a.Contains(err.Error(), "unknown delta action") + + sd["intval"] = ValueDelta{Action: SetUintAction, Uint: 0} + sd["delval"] = ValueDelta{Action: DeleteAction, Uint: 0, Bytes: tooLongValue} + err = sd.Valid(&protoF) + a.NoError(err) +} + +func TestSatisfiesSchema(t *testing.T) { + a := require.New(t) + + tkv := TealKeyValue{} + schema := StateSchema{} + err := tkv.SatisfiesSchema(schema) + a.NoError(err) + + tkv["key"] = TealValue{Type: TealType(10), Uint: 1} + err = tkv.SatisfiesSchema(schema) + a.Error(err) + a.Contains(err.Error(), "unknown type") + + tkv["key"] = TealValue{Type: TealUintType, Uint: 1} + err = tkv.SatisfiesSchema(schema) + a.Error(err) + a.Contains(err.Error(), "exceeds schema integer count") + + tkv["key"] = TealValue{Type: TealBytesType, Uint: 1, Bytes: "value"} + err = tkv.SatisfiesSchema(schema) + a.Error(err) + a.Contains(err.Error(), "exceeds schema bytes count") + + schema.NumUint = 1 + err = tkv.SatisfiesSchema(schema) + a.Error(err) + a.Contains(err.Error(), "exceeds schema bytes count") + + schema.NumByteSlice = 1 + err = tkv.SatisfiesSchema(schema) + a.NoError(err) +} diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index b696ccee57..1c93c0946a 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -41,11 +41,27 @@ const ( MaxEncodedAccountDataSize = 324205 // encodedMaxAssetsPerAccount is the decoder limit of number of assets stored per account. - // it's being verified by the unit test TestEncodedAccountAssetsAllocationBound to align + // it's being verified by the unit test TestEncodedAccountAllocationBounds to align // with config.Consensus[protocol.ConsensusCurrentVersion].MaxAssetsPerAccount; note that the decoded // parameter is used only for protecting the decoder against malicious encoded account data stream. // protocol-specific constains would be tested once the decoding is complete. encodedMaxAssetsPerAccount = 1024 + + // encodedMaxAppLocalStates is the decoder limit for number of opted-in apps in a single account. + // It is verified in TestEncodedAccountAllocationBounds to align with + // config.Consensus[protocol.ConsensusCurrentVersion].MaxppsOptedIn + encodedMaxAppLocalStates = 64 + + // encodedMaxAppParams is the decoder limit for number of created apps in a single account. + // It is verified in TestEncodedAccountAllocationBounds to align with + // config.Consensus[protocol.ConsensusCurrentVersion].MaxAppsCreated + encodedMaxAppParams = 64 + + // encodedMaxKeyValueEntries is the decoder limit for the length of a key/value store. + // It is verified in TestEncodedAccountAllocationBounds to align with + // config.Consensus[protocol.ConsensusCurrentVersion].MaxLocalSchemaEntries and + // config.Consensus[protocol.ConsensusCurrentVersion].MaxGlobalSchemaEntries + encodedMaxKeyValueEntries = 1024 ) func (s Status) String() string { @@ -153,6 +169,63 @@ type AccountData struct { // A transaction may change an account's AuthAddr to "re-key" the account. // This allows key rotation, changing the members in a multisig, etc. AuthAddr Address `codec:"spend"` + + // AppLocalStates stores the local states associated with any applications + // that this account has opted in to. + AppLocalStates map[AppIndex]AppLocalState `codec:"appl,allocbound=encodedMaxAppLocalStates"` + + // AppParams stores the global parameters and state associated with any + // applications that this account has created. + AppParams map[AppIndex]AppParams `codec:"appp,allocbound=encodedMaxAppParams"` + + // TotalAppSchema stores the sum of all of the LocalStateSchemas + // and GlobalStateSchemas in this account (global for applications + // we created local for applications we opted in to), so that we don't + // have to iterate over all of them to compute MinBalance. + TotalAppSchema StateSchema `codec:"tsch"` +} + +// AppLocalState stores the LocalState associated with an application. It also +// stores a cached copy of the application's LocalStateSchema so that +// MinBalance requirements may be computed 1. without looking up the +// AppParams and 2. even if the application has been deleted +type AppLocalState struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + Schema StateSchema `codec:"hsch"` + KeyValue TealKeyValue `codec:"tkv"` +} + +// AppParams stores the global information associated with an application +type AppParams struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + ApprovalProgram []byte `codec:"approv"` + ClearStateProgram []byte `codec:"clearp"` + LocalStateSchema StateSchema `codec:"lsch"` + GlobalStateSchema StateSchema `codec:"gsch"` + + GlobalState TealKeyValue `codec:"gs"` +} + +// Clone returns a copy of some AppParams that may be modified without +// affecting the original +func (ap *AppParams) Clone() (res AppParams) { + res = *ap + res.ApprovalProgram = make([]byte, len(ap.ApprovalProgram)) + copy(res.ApprovalProgram, ap.ApprovalProgram) + res.ClearStateProgram = make([]byte, len(ap.ClearStateProgram)) + copy(res.ClearStateProgram, ap.ClearStateProgram) + res.GlobalState = ap.GlobalState.Clone() + return +} + +// Clone returns a copy of some AppLocalState that may be modified without +// affecting the original +func (al *AppLocalState) Clone() (res AppLocalState) { + res = *al + res.KeyValue = al.KeyValue.Clone() + return } // AccountDetail encapsulates meaningful details about a given account, for external consumption @@ -181,12 +254,38 @@ type BalanceDetail struct { // up the creator of the asset, whose balance record contains the AssetParams type AssetIndex uint64 -// AssetLocator stores both the asset creator, whose balance record contains -// the asset parameters, and the asset index, which is the key into those -// parameters -type AssetLocator struct { +// AppIndex is the unique integer index of an application that can be used to +// look up the creator of the application, whose balance record contains the +// AppParams +type AppIndex uint64 + +// CreatableIndex represents either an AssetIndex or AppIndex, which come from +// the same namespace of indices as each other (both assets and apps are +// "creatables") +type CreatableIndex uint64 + +// CreatableType is an enum representing whether or not a given creatable is an +// application or an asset +type CreatableType uint64 + +const ( + // AssetCreatable is the CreatableType corresponding to assets + // This value must be 0 to align with the applications database + // upgrade. At migration time, we set the default 'ctype' column of the + // creators table to 0 so that existing assets have the correct type. + AssetCreatable CreatableType = 0 + + // AppCreatable is the CreatableType corresponds to apps + AppCreatable CreatableType = 1 +) + +// CreatableLocator stores both the creator, whose balance record contains +// the asset/app parameters, and the creatable index, which is the key into +// those parameters +type CreatableLocator struct { + Type CreatableType Creator Address - Index AssetIndex + Index CreatableIndex } // AssetHolding describes an asset held by an account. @@ -280,6 +379,36 @@ func (u AccountData) WithUpdatedRewards(proto config.ConsensusParams, rewardsLev return u } +// MinBalance computes the minimum balance requirements for an account based on +// some consensus parameters. MinBalance should correspond roughly to how much +// storage the account is allowed to store on disk. +func (u AccountData) MinBalance(proto *config.ConsensusParams) (res MicroAlgos) { + var min uint64 + + // First, base MinBalance + min = proto.MinBalance + + // MinBalance for each Asset + assetCost := MulSaturate(proto.MinBalance, uint64(len(u.Assets))) + min = AddSaturate(min, assetCost) + + // Base MinBalance for each created application + appCreationCost := MulSaturate(proto.AppFlatParamsMinBalance, uint64(len(u.AppParams))) + min = AddSaturate(min, appCreationCost) + + // Base MinBalance for each opted in application + appOptInCost := MulSaturate(proto.AppFlatOptInMinBalance, uint64(len(u.AppLocalStates))) + min = AddSaturate(min, appOptInCost) + + // MinBalance for state usage measured by LocalStateSchemas and + // GlobalStateSchemas + schemaCost := u.TotalAppSchema.MinBalance(proto) + min = AddSaturate(min, schemaCost.Raw) + + res.Raw = min + return res +} + // VotingStake returns the amount of MicroAlgos associated with the user's account // for the purpose of participating in the Algorand protocol. It assumes the // caller has already updated rewards appropriately using WithUpdatedRewards(). diff --git a/data/basics/userBalance_test.go b/data/basics/userBalance_test.go index 102b0110a2..9d0ffcf0bc 100644 --- a/data/basics/userBalance_test.go +++ b/data/basics/userBalance_test.go @@ -147,11 +147,24 @@ func TestEncodedAccountDataSize(t *testing.T) { require.Equal(t, MaxEncodedAccountDataSize, len(encoded)) } -func TestEncodedAccountAssetsAllocationBound(t *testing.T) { - // ensure that all the supported protocols have MaxAssetsPerAccount less or equal to the encodedMaxAssetsPerAccount. +func TestEncodedAccountAllocationBounds(t *testing.T) { + // ensure that all the supported protocols have value limits less or + // equal to their corresponding codec allocbounds for protoVer, proto := range config.Consensus { if proto.MaxAssetsPerAccount > encodedMaxAssetsPerAccount { require.Failf(t, "proto.MaxAssetsPerAccount > encodedMaxAssetsPerAccount", "protocol version = %s", protoVer) } + if proto.MaxAppsCreated > encodedMaxAppParams { + require.Failf(t, "proto.MaxAppsCreated > encodedMaxAppParams", "protocol version = %s", protoVer) + } + if proto.MaxAppsOptedIn > encodedMaxAppLocalStates { + require.Failf(t, "proto.MaxAppsOptedIn > encodedMaxAppLocalStates", "protocol version = %s", protoVer) + } + if proto.MaxLocalSchemaEntries > encodedMaxKeyValueEntries { + require.Failf(t, "proto.MaxLocalSchemaEntries > encodedMaxKeyValueEntries", "protocol version = %s", protoVer) + } + if proto.MaxGlobalSchemaEntries > encodedMaxKeyValueEntries { + require.Failf(t, "proto.MaxGlobalSchemaEntries > encodedMaxKeyValueEntries", "protocol version = %s", protoVer) + } } } diff --git a/data/transactions/application.go b/data/transactions/application.go new file mode 100644 index 0000000000..c6852364ee --- /dev/null +++ b/data/transactions/application.go @@ -0,0 +1,791 @@ +// 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 . + +package transactions + +import ( + "fmt" + + "github.com/algorand/go-algorand/data/basics" +) + +const ( + // encodedMaxApplicationArgs sets the allocation bound for the maximum + // number of ApplicationArgs that a transaction decoded off of the wire + // can contain. Its value is verified against consensus parameters in + // TestEncodedAppTxnAllocationBounds + encodedMaxApplicationArgs = 32 + + // encodedMaxAccounts sets the allocation bound for the maximum number + // of Accounts that a transaction decoded off of the wire can contain. + // Its value is verified against consensus parameters in + // TestEncodedAppTxnAllocationBounds + encodedMaxAccounts = 32 + + // encodedMaxForeignApps sets the allocation bound for the maximum + // number of ForeignApps that a transaction decoded off of the wire can + // contain. Its value is verified against consensus parameters in + // TestEncodedAppTxnAllocationBounds + encodedMaxForeignApps = 32 +) + +// OnCompletion is an enum representing some layer 1 side effect that an +// ApplicationCall transaction will have if it is included in a block. +//go:generate stringer -type=OnCompletion -output=application_string.go +type OnCompletion uint64 + +const ( + // NoOpOC indicates that an application transaction will simply call its + // ApprovalProgram + NoOpOC OnCompletion = 0 + + // OptInOC indicates that an application transaction will allocate some + // LocalState for the application in the sender's account + OptInOC OnCompletion = 1 + + // CloseOutOC indicates that an application transaction will deallocate + // some LocalState for the application from the user's account + CloseOutOC OnCompletion = 2 + + // ClearStateOC is similar to CloseOutOC, but may never fail. This + // allows users to reclaim their minimum balance from an application + // they no longer wish to opt in to. + ClearStateOC OnCompletion = 3 + + // UpdateApplicationOC indicates that an application transaction will + // update the ApprovalProgram and ClearStateProgram for the application + UpdateApplicationOC OnCompletion = 4 + + // DeleteApplicationOC indicates that an application transaction will + // delete the AppParams for the application from the creator's balance + // record + DeleteApplicationOC OnCompletion = 5 +) + +// ApplicationCallTxnFields captures the transaction fields used for all +// interactions with applications +type ApplicationCallTxnFields struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + ApplicationID basics.AppIndex `codec:"apid"` + OnCompletion OnCompletion `codec:"apan"` + ApplicationArgs [][]byte `codec:"apaa,allocbound=encodedMaxApplicationArgs"` + Accounts []basics.Address `codec:"apat,allocbound=encodedMaxAccounts"` + ForeignApps []basics.AppIndex `codec:"apfa,allocbound=encodedMaxForeignApps"` + + LocalStateSchema basics.StateSchema `codec:"apls"` + GlobalStateSchema basics.StateSchema `codec:"apgs"` + ApprovalProgram []byte `codec:"apap"` + ClearStateProgram []byte `codec:"apsu"` + + // If you add any fields here, remember you MUST modify the Empty + // method below! +} + +// Empty indicates whether or not all the fields in the +// ApplicationCallTxnFields are zeroed out +func (ac *ApplicationCallTxnFields) Empty() bool { + if ac.ApplicationID != 0 { + return false + } + if ac.OnCompletion != 0 { + return false + } + if ac.ApplicationArgs != nil { + return false + } + if ac.Accounts != nil { + return false + } + if ac.ForeignApps != nil { + return false + } + if ac.LocalStateSchema != (basics.StateSchema{}) { + return false + } + if ac.GlobalStateSchema != (basics.StateSchema{}) { + return false + } + if ac.ApprovalProgram != nil { + return false + } + if ac.ClearStateProgram != nil { + return false + } + return true +} + +// Allocate the map of LocalStates if it is nil, and return a copy. We do *not* +// call clone on each AppLocalState -- callers must do that for any values +// where they intend to modify a contained reference type e.g. KeyValue. +func cloneAppLocalStates(m map[basics.AppIndex]basics.AppLocalState) map[basics.AppIndex]basics.AppLocalState { + res := make(map[basics.AppIndex]basics.AppLocalState, len(m)) + for k, v := range m { + res[k] = v + } + return res +} + +// Allocate the map of AppParams if it is nil, and return a copy. We do *not* +// call clone on each AppParams -- callers must do that for any values where +// they intend to modify a contained reference type e.g. the GlobalState. +func cloneAppParams(m map[basics.AppIndex]basics.AppParams) map[basics.AppIndex]basics.AppParams { + res := make(map[basics.AppIndex]basics.AppParams, len(m)) + for k, v := range m { + res[k] = v + } + return res +} + +// getAppParams fetches the creator address and AppParams for the app index, +// if they exist. It does *not* clone the AppParams, so the returned params +// must not be modified directly. +func getAppParams(balances Balances, aidx basics.AppIndex) (params basics.AppParams, creator basics.Address, exists bool, err error) { + creator, exists, err = balances.GetAppCreator(aidx) + if err != nil { + return + } + + // App doesn't exist. Not an error, but return straight away + if !exists { + return + } + + record, err := balances.Get(creator, false) + if err != nil { + return + } + + params, ok := record.AppParams[aidx] + if !ok { + // This should never happen. If app exists then we should have + // found the creator successfully. + err = fmt.Errorf("app %d not found in account %s", aidx, creator.String()) + return + } + + return +} + +func applyStateDelta(kv basics.TealKeyValue, stateDelta basics.StateDelta) error { + if kv == nil { + return fmt.Errorf("cannot apply delta to nil TealKeyValue") + } + + // Because the keys of stateDelta each correspond to one existing/new + // key in the key/value store, there can be at most one delta per key. + // Therefore the order that the deltas are applied does not matter. + for key, valueDelta := range stateDelta { + switch valueDelta.Action { + case basics.SetUintAction: + kv[key] = basics.TealValue{ + Type: basics.TealUintType, + Uint: valueDelta.Uint, + } + case basics.SetBytesAction: + kv[key] = basics.TealValue{ + Type: basics.TealBytesType, + Bytes: valueDelta.Bytes, + } + case basics.DeleteAction: + delete(kv, key) + default: + return fmt.Errorf("unknown delta action %d", valueDelta.Action) + } + } + return nil +} + +// applyEvalDelta applies a basics.EvalDelta to the app's global key/value +// store as well as a set of local key/value stores. If this function returns +// an error, the transaction must not be committed. +// +// The errIfNotApplied parameter is set to false when applying the results of a +// ClearState program. ClearState programs are not allowed to reject +// transactions for any reason. So if the ClearState program passes, +// but returns an invalid evalDelta that we cannot apply (e.g. because it would +// violate a schema), then errIfNotApplied = false instructs applyEvalDelta to +// return a nil error. For system errors (e.g. failing to fetch or write a +// balance record), applyEvalDelta will always return a non-nil error. +func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, params basics.AppParams, creator, sender basics.Address, balances Balances, appIdx basics.AppIndex, errIfNotApplied bool) error { + /* + * 1. Apply GlobalState delta (if any), allocating the key/value store + * if required. + */ + + proto := balances.ConsensusParams() + if len(evalDelta.GlobalDelta) > 0 { + // Clone the parameters so that they are safe to modify + params = params.Clone() + + // Allocate GlobalState if necessary. We need to do this now + // since an empty map will be read as nil from disk + if params.GlobalState == nil { + params.GlobalState = make(basics.TealKeyValue) + } + + // Check that the global state delta isn't breaking any rules regarding + // key/value lengths + err := evalDelta.GlobalDelta.Valid(&proto) + if err != nil { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("cannot apply GlobalState delta: %v", err) + } + + // Apply the GlobalDelta in place on the cloned copy + err = applyStateDelta(params.GlobalState, evalDelta.GlobalDelta) + if err != nil { + return err + } + + // Make sure we haven't violated the GlobalStateSchema + err = params.GlobalState.SatisfiesSchema(params.GlobalStateSchema) + if err != nil { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("GlobalState for app %d would use too much space: %v", appIdx, err) + } + } + + /* + * 2. Apply each LocalState delta, fail fast if any affected account + * has not opted in to appIdx or would violate the LocalStateSchema. + * Don't write anything back to the cow yet. + */ + + changes := make(map[basics.Address]basics.AppLocalState, len(evalDelta.LocalDeltas)) + for accountIdx, delta := range evalDelta.LocalDeltas { + // LocalDeltas are keyed by account index [sender, tx.Accounts[0], ...] + addr, err := ac.AddressByIndex(accountIdx, sender) + if err != nil { + return err + } + + // Ensure we did not already receive a non-empty LocalState + // delta for this address, in case the caller passed us an + // invalid EvalDelta + _, ok := changes[addr] + if ok { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("duplicate LocalState delta for %s", addr.String()) + } + + // Zero-length LocalState deltas are not allowed. We should never produce + // them from Eval. + if len(delta) == 0 { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("got zero-length delta for %s, not allowed", addr.String()) + } + + // Check that the local state delta isn't breaking any rules regarding + // key/value lengths + err = delta.Valid(&proto) + if err != nil { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("cannot apply LocalState delta for %s: %v", addr.String(), err) + } + + record, err := balances.Get(addr, false) + if err != nil { + return err + } + + localState, ok := record.AppLocalStates[appIdx] + if !ok { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("cannot apply LocalState delta to %s: acct has not opted in to app %d", addr.String(), appIdx) + } + + // Clone LocalState so that we have a copy that is safe to modify + localState = localState.Clone() + + // Allocate localState.KeyValue if necessary. We need to do + // this now since an empty map will be read as nil from disk + if localState.KeyValue == nil { + localState.KeyValue = make(basics.TealKeyValue) + } + + err = applyStateDelta(localState.KeyValue, delta) + if err != nil { + return err + } + + // Make sure we haven't violated the LocalStateSchema + err = localState.KeyValue.SatisfiesSchema(localState.Schema) + if err != nil { + if !errIfNotApplied { + return nil + } + return fmt.Errorf("LocalState for %s for app %d would use too much space: %v", addr.String(), appIdx, err) + } + + // Stage the change to be committed after all schema checks + changes[addr] = localState + } + + /* + * 3. Write any GlobalState changes back to cow. This should be correct + * even if creator is in the local deltas, because the updated + * fields are different. + */ + + if len(evalDelta.GlobalDelta) > 0 { + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Overwrite parameters for this appIdx with our cloned, + // modified params + record.AppParams = cloneAppParams(record.AppParams) + record.AppParams[appIdx] = params + + err = balances.Put(record) + if err != nil { + return err + } + } + + /* + * 4. Write LocalState changes back to cow + */ + + for addr, newLocalState := range changes { + record, err := balances.Get(addr, false) + if err != nil { + return err + } + + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + record.AppLocalStates[appIdx] = newLocalState + + err = balances.Put(record) + if err != nil { + return err + } + } + + return nil +} + +func (ac *ApplicationCallTxnFields) checkPrograms(steva StateEvaluator, maxCost int) error { + cost, err := steva.Check(ac.ApprovalProgram) + if err != nil { + return fmt.Errorf("check failed on ApprovalProgram: %v", err) + } + + if cost > maxCost { + return fmt.Errorf("ApprovalProgram too resource intensive. Cost is %d, max %d", cost, maxCost) + } + + cost, err = steva.Check(ac.ClearStateProgram) + if err != nil { + return fmt.Errorf("check failed on ClearStateProgram: %v", err) + } + + if cost > maxCost { + return fmt.Errorf("ClearStateProgram too resource intensive. Cost is %d, max %d", cost, maxCost) + } + + return nil +} + +// AddressByIndex converts an integer index into an address associated with the +// transaction. Index 0 corresponds to the transaction sender, and an index > 0 +// corresponds to an offset into txn.Accounts. Returns an error if the index is +// not valid. +func (ac *ApplicationCallTxnFields) AddressByIndex(accountIdx uint64, sender basics.Address) (basics.Address, error) { + // Index 0 always corresponds to the sender + if accountIdx == 0 { + return sender, nil + } + + // An index > 0 corresponds to an offset into txn.Accounts. Check to + // make sure the index is valid. + if accountIdx > uint64(len(ac.Accounts)) { + err := fmt.Errorf("cannot load account[%d] of %d", accountIdx, len(ac.Accounts)) + return basics.Address{}, err + } + + // accountIdx must be in [1, len(ac.Accounts)] + return ac.Accounts[accountIdx-1], nil +} + +// createApplication writes a new AppParams entry and returns application ID +func (ac *ApplicationCallTxnFields) createApplication( + balances Balances, creator basics.Address, txnCounter uint64, +) (appIdx basics.AppIndex, err error) { + + // Fetch the creator's (sender's) balance record + record, err := balances.Get(creator, false) + if err != nil { + return + } + + // Make sure the creator isn't already at the app creation max + maxAppsCreated := balances.ConsensusParams().MaxAppsCreated + if len(record.AppParams) >= maxAppsCreated { + err = fmt.Errorf("cannot create app for %s: max created apps per acct is %d", creator.String(), maxAppsCreated) + return + } + + // Clone app params, so that we have a copy that is safe to modify + record.AppParams = cloneAppParams(record.AppParams) + + // Allocate the new app params (+ 1 to match Assets Idx namespace) + appIdx = basics.AppIndex(txnCounter + 1) + record.AppParams[appIdx] = basics.AppParams{ + ApprovalProgram: ac.ApprovalProgram, + ClearStateProgram: ac.ClearStateProgram, + LocalStateSchema: ac.LocalStateSchema, + GlobalStateSchema: ac.GlobalStateSchema, + } + + // Update the cached TotalStateSchema for this account, used + // when computing MinBalance, since the creator has to store + // the global state + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.AddSchema(ac.GlobalStateSchema) + record.TotalAppSchema = totalSchema + + // 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 and continue + err = balances.PutWithCreatables(record, created, nil) + if err != nil { + return 0, err + } + + return +} + +func (ac *ApplicationCallTxnFields) applyClearState( + balances Balances, sender basics.Address, appIdx basics.AppIndex, + ad *ApplyData, steva StateEvaluator, +) error { + // Fetch the application parameters, if they exist + params, creator, exists, err := getAppParams(balances, appIdx) + if err != nil { + return err + } + + record, err := balances.Get(sender, false) + if err != nil { + return err + } + + // Ensure sender actually has LocalState allocated for this app. + // Can't clear out if not currently opted in + _, ok := record.AppLocalStates[appIdx] + if !ok { + return fmt.Errorf("cannot clear state for app %d: account %s is not currently opted in", appIdx, sender.String()) + } + + // If the application still exists... + if exists { + // Execute the ClearStateProgram before we've deleted the LocalState + // for this account. If the ClearStateProgram does not fail, apply any + // state deltas it generated. + pass, evalDelta, err := steva.Eval(params.ClearStateProgram) + if err == nil && pass { + // Program execution may produce some GlobalState and LocalState + // deltas. Apply them, provided they don't exceed the bounds set by + // the GlobalStateSchema and LocalStateSchema. If they do exceed + // those bounds, then don't fail, but also don't apply the changes. + failIfNotApplied := false + err = ac.applyEvalDelta(evalDelta, params, creator, sender, + balances, appIdx, failIfNotApplied) + if err != nil { + return err + } + + // Fill in applyData, so that consumers don't have to implement a + // stateful TEAL interpreter to apply state changes + ad.EvalDelta = evalDelta + } else { + // Ignore errors and rejections from the ClearStateProgram + } + + // Fetch the (potentially updated) sender record + record, err = balances.Get(sender, false) + if err != nil { + ad.EvalDelta = basics.EvalDelta{} + return err + } + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender no longer has to store LocalState + totalSchema := record.TotalAppSchema + localSchema := record.AppLocalStates[appIdx].Schema + totalSchema = totalSchema.SubSchema(localSchema) + record.TotalAppSchema = totalSchema + + // Deallocate the AppLocalState and finish + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + delete(record.AppLocalStates, appIdx) + + return balances.Put(record) +} + +func applyOptIn(balances Balances, sender basics.Address, appIdx basics.AppIndex, params basics.AppParams) error { + record, err := balances.Get(sender, false) + if err != nil { + return err + } + + // If the user has already opted in, fail + _, ok := record.AppLocalStates[appIdx] + if ok { + return fmt.Errorf("account %s has already opted in to app %d", sender.String(), appIdx) + } + + // Make sure the user isn't already at the app opt-in max + maxAppsOptedIn := balances.ConsensusParams().MaxAppsOptedIn + if len(record.AppLocalStates) >= maxAppsOptedIn { + return fmt.Errorf("cannot opt in app %d for %s: max opted-in apps per acct is %d", appIdx, sender.String(), maxAppsOptedIn) + } + + // If the user hasn't opted in yet, allocate LocalState for the app + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + record.AppLocalStates[appIdx] = basics.AppLocalState{ + Schema: params.LocalStateSchema, + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender must now store LocalState + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.AddSchema(params.LocalStateSchema) + record.TotalAppSchema = totalSchema + + return balances.Put(record) +} + +func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData, txnCounter uint64, steva StateEvaluator) (err error) { + defer func() { + // If we are returning a non-nil error, then don't return a + // non-empty EvalDelta. Not required for correctness. + if err != nil && ad != nil { + ad.EvalDelta = basics.EvalDelta{} + } + }() + + // Keep track of the application ID we're working on + appIdx := ac.ApplicationID + + // this is not the case in the current code but still probably better to check + if ad == nil { + err = fmt.Errorf("cannot use empty ApplyData") + return + } + + // Specifying an application ID of 0 indicates application creation + if ac.ApplicationID == 0 { + appIdx, err = ac.createApplication(balances, header.Sender, txnCounter) + if err != nil { + return + } + } + + // Initialize our TEAL evaluation context. Internally, this manages + // access to balance records for Stateful TEAL programs. Stateful TEAL + // may only access + // - The sender's balance record + // - The balance records of accounts explicitly listed in ac.Accounts + // - The app creator's balance record (to read/write GlobalState) + // - The balance records of creators of apps in ac.ForeignApps (to read + // GlobalState) + acctWhitelist := append(ac.Accounts, header.Sender) + appGlobalWhitelist := append(ac.ForeignApps, appIdx) + err = steva.InitLedger(balances, acctWhitelist, appGlobalWhitelist, appIdx) + if err != nil { + return err + } + + // If this txn is going to set new programs (either for creation or + // update), check that the programs are valid and not too expensive + if ac.ApplicationID == 0 || ac.OnCompletion == UpdateApplicationOC { + maxCost := balances.ConsensusParams().MaxAppProgramCost + err = ac.checkPrograms(steva, maxCost) + if err != nil { + return err + } + } + + // Clear out our LocalState. In this case, we don't execute the + // ApprovalProgram, since clearing out is always allowed. We only + // execute the ClearStateProgram, whose failures are ignored. + if ac.OnCompletion == ClearStateOC { + return ac.applyClearState(balances, header.Sender, appIdx, ad, steva) + } + + // Fetch the application parameters, if they exist + params, creator, exists, err := getAppParams(balances, appIdx) + if err != nil { + return err + } + + // Past this point, the AppParams must exist. NoOp, OptIn, CloseOut, + // Delete, and Update + if !exists { + return fmt.Errorf("only clearing out is supported for applications that do not exist") + } + + // If this is an OptIn transaction, ensure that the sender has + // LocalState allocated prior to TEAL execution, so that it may be + // initialized in the same transaction. + if ac.OnCompletion == OptInOC { + err = applyOptIn(balances, header.Sender, appIdx, params) + if err != nil { + return err + } + } + + // Execute the Approval program + approved, evalDelta, err := steva.Eval(params.ApprovalProgram) + if err != nil { + return err + } + + if !approved { + return fmt.Errorf("transaction rejected by ApprovalProgram") + } + + // Apply GlobalState and LocalState deltas, provided they don't exceed + // the bounds set by the GlobalStateSchema and LocalStateSchema. + // If they would exceed those bounds, then fail. + failIfNotApplied := true + err = ac.applyEvalDelta(evalDelta, params, creator, header.Sender, + balances, appIdx, failIfNotApplied) + if err != nil { + return err + } + + switch ac.OnCompletion { + case NoOpOC: + // Nothing to do + + case OptInOC: + // Handled above + + case CloseOutOC: + // Closing out of the application. Fetch the sender's balance record + record, err := balances.Get(header.Sender, false) + if err != nil { + return err + } + + // If they haven't opted in, that's an error + localState, ok := record.AppLocalStates[appIdx] + if !ok { + return fmt.Errorf("account %s is not opted in to app %d", header.Sender.String(), appIdx) + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender no longer has to store LocalState + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.SubSchema(localState.Schema) + record.TotalAppSchema = totalSchema + + // Delete the local state + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + delete(record.AppLocalStates, appIdx) + + err = balances.Put(record) + if err != nil { + return err + } + + case DeleteApplicationOC: + // Deleting the application. Fetch the creator's balance record + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the creator no longer has to store the GlobalState + totalSchema := record.TotalAppSchema + globalSchema := record.AppParams[appIdx].GlobalStateSchema + totalSchema = totalSchema.SubSchema(globalSchema) + record.TotalAppSchema = totalSchema + + // Delete the AppParams + record.AppParams = cloneAppParams(record.AppParams) + delete(record.AppParams, appIdx) + + // Tell the cow what app we deleted + deleted := []basics.CreatableLocator{ + basics.CreatableLocator{ + Creator: header.Sender, + Type: basics.AppCreatable, + Index: basics.CreatableIndex(appIdx), + }, + } + + // Write back to cow + err = balances.PutWithCreatables(record, nil, deleted) + if err != nil { + return err + } + + case UpdateApplicationOC: + // Updating the application. Fetch the creator's balance record + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Fill in the new programs + record.AppParams = cloneAppParams(record.AppParams) + params := record.AppParams[appIdx] + params.ApprovalProgram = ac.ApprovalProgram + params.ClearStateProgram = ac.ClearStateProgram + + record.AppParams[appIdx] = params + err = balances.Put(record) + if err != nil { + return err + } + + default: + return fmt.Errorf("invalid application action") + } + + // Fill in applyData, so that consumers don't have to implement a + // stateful TEAL interpreter to apply state changes + ad.EvalDelta = evalDelta + + return nil +} diff --git a/data/transactions/application_string.go b/data/transactions/application_string.go new file mode 100644 index 0000000000..8548b45445 --- /dev/null +++ b/data/transactions/application_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type=OnCompletion -output=application_string.go"; DO NOT EDIT. + +package transactions + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[NoOpOC-0] + _ = x[OptInOC-1] + _ = x[CloseOutOC-2] + _ = x[ClearStateOC-3] + _ = x[UpdateApplicationOC-4] + _ = x[DeleteApplicationOC-5] +} + +const _OnCompletion_name = "NoOpOCOptInOCCloseOutOCClearStateOCUpdateApplicationOCDeleteApplicationOC" + +var _OnCompletion_index = [...]uint8{0, 6, 13, 23, 35, 54, 73} + +func (i OnCompletion) String() string { + if i >= OnCompletion(len(_OnCompletion_index)-1) { + return "OnCompletion(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _OnCompletion_name[_OnCompletion_index[i]:_OnCompletion_index[i+1]] +} diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go new file mode 100644 index 0000000000..8fd48b3c40 --- /dev/null +++ b/data/transactions/application_test.go @@ -0,0 +1,1404 @@ +// 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 . + +package transactions + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" +) + +func TestApplicationCallFieldsNotChanged(t *testing.T) { + af := ApplicationCallTxnFields{} + s := reflect.ValueOf(&af).Elem() + + if s.NumField() != 10 { + t.Errorf("You added or removed a field from ApplicationCallTxnFields. " + + "Please ensure you have updated the Empty() method and then " + + "fix this test") + } +} + +func TestApplicationCallFieldsEmpty(t *testing.T) { + a := require.New(t) + + ac := ApplicationCallTxnFields{} + a.True(ac.Empty()) + + ac.ApplicationID = 1 + a.False(ac.Empty()) + + ac.ApplicationID = 0 + ac.OnCompletion = 1 + a.False(ac.Empty()) + + ac.OnCompletion = 0 + ac.ApplicationArgs = make([][]byte, 1) + a.False(ac.Empty()) + + ac.ApplicationArgs = nil + ac.Accounts = make([]basics.Address, 1) + a.False(ac.Empty()) + + ac.Accounts = nil + ac.ForeignApps = make([]basics.AppIndex, 1) + a.False(ac.Empty()) + + ac.ForeignApps = nil + ac.LocalStateSchema = basics.StateSchema{NumUint: 1} + a.False(ac.Empty()) + + ac.LocalStateSchema = basics.StateSchema{} + ac.GlobalStateSchema = basics.StateSchema{NumUint: 1} + a.False(ac.Empty()) + + ac.GlobalStateSchema = basics.StateSchema{} + ac.ApprovalProgram = []byte{1} + a.False(ac.Empty()) + + ac.ApprovalProgram = []byte{} + a.False(ac.Empty()) + + ac.ApprovalProgram = nil + ac.ClearStateProgram = []byte{1} + a.False(ac.Empty()) + + ac.ClearStateProgram = []byte{} + a.False(ac.Empty()) + + ac.ClearStateProgram = nil + a.True(ac.Empty()) +} + +func getRandomAddress(a *require.Assertions) basics.Address { + const rl = 16 + b := make([]byte, rl) + n, err := rand.Read(b) + a.NoError(err) + a.Equal(rl, n) + + address := crypto.Hash(b) + return basics.Address(address) +} + +type testBalances struct { + appCreators map[basics.AppIndex]basics.Address + balances map[basics.Address]basics.AccountData + proto config.ConsensusParams + + put int // Put calls counter + putWith int // PutWithCreatables calls counter + putBalances map[basics.Address]basics.AccountData + putWithBalances map[basics.Address]basics.AccountData + putWithNew []basics.CreatableLocator + putWithDel []basics.CreatableLocator +} + +type testBalancesPass struct { + testBalances +} + +const appIdxError basics.AppIndex = 0x11223344 +const appIdxOk basics.AppIndex = 1 + +func (b *testBalances) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalances) Put(record basics.BalanceRecord) error { + b.put++ + if b.putBalances == nil { + b.putBalances = make(map[basics.Address]basics.AccountData) + } + b.putBalances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalances) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { + b.putWith++ + if b.putWithBalances == nil { + b.putWithBalances = make(map[basics.Address]basics.AccountData) + } + b.putWithBalances[record.Addr] = record.AccountData + b.putWithNew = append(b.putWithNew, newCreatables...) + b.putWithDel = append(b.putWithDel, deletedCreatables...) + return nil +} + +func (b *testBalances) GetAssetCreator(aidx basics.AssetIndex) (basics.Address, error) { + return basics.Address{}, nil +} + +func (b *testBalances) GetAppCreator(aidx basics.AppIndex) (basics.Address, bool, error) { + if aidx == appIdxError { // magic for test + return basics.Address{}, false, fmt.Errorf("mock synthetic error") + } + + creator, ok := b.appCreators[aidx] + return creator, ok, nil +} + +func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { + return nil +} + +func (b *testBalancesPass) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalancesPass) Put(record basics.BalanceRecord) error { + if b.balances == nil { + b.balances = make(map[basics.Address]basics.AccountData) + } + b.balances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalancesPass) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { + if b.balances == nil { + b.balances = make(map[basics.Address]basics.AccountData) + } + b.balances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalances) ConsensusParams() config.ConsensusParams { + return b.proto +} + +// ResetWrites clears side effects of Put/PutWithCreatables +func (b *testBalances) ResetWrites() { + b.put = 0 + b.putWith = 0 + b.putBalances = nil + b.putWithBalances = nil + b.putWithNew = []basics.CreatableLocator{} + b.putWithDel = []basics.CreatableLocator{} +} + +func (b *testBalances) SetProto(name protocol.ConsensusVersion) { + b.proto = config.Consensus[name] +} + +type testEvaluator struct { + pass bool + delta basics.EvalDelta + + acctWhitelist []basics.Address + appGlobalWhitelist []basics.AppIndex + appIdx basics.AppIndex +} + +// Eval for tests that fail on program version > 10 and returns pass/delta from its own state rather than running the program +func (e *testEvaluator) Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) { + if len(program) < 1 || program[0] > 10 { + return false, basics.EvalDelta{}, fmt.Errorf("mock eval error") + } + return e.pass, e.delta, nil +} + +// Check for tests that fail on program version > 10 and returns program len as cost +func (e *testEvaluator) Check(program []byte) (cost int, err error) { + if len(program) < 1 || program[0] > 10 { + return 0, fmt.Errorf("mock check error") + } + return len(program), nil +} + +func (e *testEvaluator) InitLedger(balances Balances, acctWhitelist []basics.Address, appGlobalWhitelist []basics.AppIndex, appIdx basics.AppIndex) error { + e.appIdx = appIdx + e.acctWhitelist = acctWhitelist + e.appGlobalWhitelist = appGlobalWhitelist + return nil +} + +func TestAppCallApplyDelta(t *testing.T) { + a := require.New(t) + + var tkv basics.TealKeyValue + var sd basics.StateDelta + err := applyStateDelta(tkv, sd) + a.Error(err) + a.Contains(err.Error(), "cannot apply delta to nil TealKeyValue") + + tkv = basics.TealKeyValue{} + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 0) + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.DeltaAction(10), + Uint: 0, + }, + } + + err = applyStateDelta(tkv, sd) + a.Error(err) + a.Contains(err.Error(), "unknown delta action") + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetUintAction, + Uint: 1, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(uint64(1), tkv["test"].Uint) + a.Equal(basics.TealUintType, tkv["test"].Type) + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.DeleteAction, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 0) + + // nil bytes + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetBytesAction, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(basics.TealBytesType, tkv["test"].Type) + a.Equal("", tkv["test"].Bytes) + a.Equal(uint64(0), tkv["test"].Uint) + + // check illformed update + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetBytesAction, + Uint: 1, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(basics.TealBytesType, tkv["test"].Type) + a.Equal("", tkv["test"].Bytes) + a.Equal(uint64(0), tkv["test"].Uint) +} + +func TestAppCallCloneEmpty(t *testing.T) { + a := require.New(t) + + var ls map[basics.AppIndex]basics.AppLocalState + cls := cloneAppLocalStates(ls) + a.Equal(0, len(cls)) + + var ap map[basics.AppIndex]basics.AppParams + cap := cloneAppParams(ap) + a.Equal(0, len(cap)) +} + +func TestAppCallGetParam(t *testing.T) { + a := require.New(t) + + var b testBalances + _, _, _, err := getAppParams(&b, appIdxError) + a.Error(err) + + _, _, exist, err := getAppParams(&b, appIdxOk) + a.NoError(err) + a.False(exist) + + creator := getRandomAddress(a) + addr := getRandomAddress(a) + b.appCreators = map[basics.AppIndex]basics.Address{appIdxOk: creator} + b.balances = map[basics.Address]basics.AccountData{addr: {}} + _, _, exist, err = getAppParams(&b, appIdxOk) + a.Error(err) + a.True(exist) + + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{}, + } + _, _, exist, err = getAppParams(&b, appIdxOk) + a.Error(err) + a.True(exist) + + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdxOk: {}, + }, + } + params, cr, exist, err := getAppParams(&b, appIdxOk) + a.NoError(err) + a.True(exist) + a.Equal(creator, cr) + a.Equal(basics.AppParams{}, params) +} + +func TestAppCallAddressByIndex(t *testing.T) { + a := require.New(t) + + sender := getRandomAddress(a) + var ac ApplicationCallTxnFields + addr, err := ac.AddressByIndex(0, sender) + a.NoError(err) + a.Equal(sender, addr) + + addr, err = ac.AddressByIndex(1, sender) + a.Error(err) + a.Contains(err.Error(), "cannot load account[1]") + a.Equal(0, len(ac.Accounts)) + + acc0 := getRandomAddress(a) + ac.Accounts = []basics.Address{acc0} + addr, err = ac.AddressByIndex(1, sender) + a.NoError(err) + a.Equal(acc0, addr) + + addr, err = ac.AddressByIndex(2, sender) + a.Error(err) + a.Contains(err.Error(), "cannot load account[2]") +} + +func TestAppCallCheckPrograms(t *testing.T) { + a := require.New(t) + + var ac ApplicationCallTxnFields + var steva testEvaluator + + err := ac.checkPrograms(&steva, 1) + a.Error(err) + a.Contains(err.Error(), "check failed on ApprovalProgram") + + program := []byte{2, 0x20, 1, 1, 0x22} // version, intcb, int 1 + ac.ApprovalProgram = program + err = ac.checkPrograms(&steva, 1) + a.Error(err) + a.Contains(err.Error(), "ApprovalProgram too resource intensive") + + err = ac.checkPrograms(&steva, 10) + a.Error(err) + a.Contains(err.Error(), "check failed on ClearStateProgram") + + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + err = ac.checkPrograms(&steva, 10) + a.Error(err) + a.Contains(err.Error(), "ClearStateProgram too resource intensive") + + ac.ClearStateProgram = program + err = ac.checkPrograms(&steva, 10) + a.NoError(err) +} + +func TestAppCallApplyGlobalStateDeltas(t *testing.T) { + a := require.New(t) + + var creator basics.Address + var sender basics.Address + var ac ApplicationCallTxnFields + var ed basics.EvalDelta + var params basics.AppParams + var appIdx basics.AppIndex + var b testBalances + var errIfNotApplied = false + + // check empty input + err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + ed.GlobalDelta = make(basics.StateDelta) + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} + + // check global on unsupported proto + b.SetProto(protocol.ConsensusV23) + errIfNotApplied = false + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check global on supported proto + b.SetProto(protocol.ConsensusFuture) + errIfNotApplied = false + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check Action=Delete delta on empty params + errIfNotApplied = false + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.DeleteAction} + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "balance not found") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // simulate balances.GetAppCreator and balances.Get get out of sync + // creator received from balances.GetAppCreator and has app params + // and its balances.Get record is out of sync/not initialized + // ensure even if AppParams were allocated they are empty + creator = getRandomAddress(a) + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + pad, ok := b.putBalances[creator] + a.True(ok) + // There is a side effect: Action=Delete bypasses all default checks and + // forces AppParams (empty before) to have an entry for appIdx + ap, ok := pad.AppParams[appIdx] + a.True(ok) + a.Equal(0, len(ap.GlobalState)) + // ensure AppParams with pre-allocated fields is stored as empty AppParams{} + enc := protocol.Encode(&ap) + emp := protocol.Encode(&basics.AppParams{}) + a.Equal(len(emp), len(enc)) + // ensure original balance record in the mock was not changed + // this ensure proper cloning and any in-intended in-memory modifications + a.Equal(basics.AccountData{}, b.balances[creator]) + + b.ResetWrites() + + // now check errors with non-default values + b.SetProto(protocol.ConsensusV23) + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + b.SetProto(protocol.ConsensusFuture) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store integer count", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + // try illformed delta + params.GlobalStateSchema = basics.StateSchema{NumUint: 1} + ed.GlobalDelta["bytes"] = basics.ValueDelta{Action: basics.SetBytesAction, Uint: 1} + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store bytes count", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + // check a happy case + params.GlobalStateSchema = basics.StateSchema{NumUint: 1, NumByteSlice: 1} + br := 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 + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(1, b.put) + pad, ok = b.putBalances[creator] + a.True(ok) + ap, ok = pad.AppParams[appIdx] + a.True(ok) + a.Equal(2, len(ap.GlobalState)) + a.Equal(basics.TealBytesType, ap.GlobalState["bytes"].Type) + a.Equal(basics.TealUintType, ap.GlobalState["uint"].Type) + a.Equal(uint64(0), ap.GlobalState["bytes"].Uint) + a.Equal(uint64(1), ap.GlobalState["uint"].Uint) + a.Equal("", ap.GlobalState["bytes"].Bytes) + a.Equal("", ap.GlobalState["uint"].Bytes) + a.Equal(br, b.balances[creator]) +} + +func TestAppCallApplyLocalsStateDeltas(t *testing.T) { + a := require.New(t) + + var creator basics.Address = getRandomAddress(a) + var sender basics.Address = getRandomAddress(a) + var ac ApplicationCallTxnFields + var ed basics.EvalDelta + var params basics.AppParams + var appIdx basics.AppIndex + var b testBalances + var errIfNotApplied = false + + b.balances = make(map[basics.Address]basics.AccountData) + ed.LocalDeltas = make(map[uint64]basics.StateDelta) + + err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + ed.LocalDeltas[1] = basics.StateDelta{} + + // non-existing account + errIfNotApplied = false + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + + // empty delta + ac.Accounts = append(ac.Accounts, sender, sender) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // test duplicates in accounts + b.SetProto(protocol.ConsensusFuture) + ed.LocalDeltas[0] = basics.StateDelta{"uint": basics.ValueDelta{Action: basics.DeleteAction}} + ed.LocalDeltas[1] = basics.StateDelta{"bytes": basics.ValueDelta{Action: basics.DeleteAction}} + b.balances[sender] = basics.AccountData{} + errIfNotApplied = false + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[sender]) + // not opted in + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "acct has not opted in to app") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + // ensure original balance record in the mock was not changed + // this ensure proper cloning and any in-intended in-memory modifications + a.Equal(basics.AccountData{}, b.balances[sender]) + + states := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} + cp := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} + b.balances[sender] = basics.AccountData{ + AppLocalStates: cp, + } + errIfNotApplied = true + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "duplicate LocalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + // ensure no changes in original balance record + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + + // test valid deltas and accounts + ac.Accounts = nil + states = map[basics.AppIndex]basics.AppLocalState{appIdx: {}} + b.balances[sender] = basics.AccountData{AppLocalStates: states} + ed.LocalDeltas[0] = basics.StateDelta{ + "uint": basics.ValueDelta{Action: basics.SetUintAction, Uint: 1}, + "bytes": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "value"}, + } + delete(ed.LocalDeltas, 1) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.Error(err) + a.Contains(err.Error(), "would use too much space") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + + // happy case + states = map[basics.AppIndex]basics.AppLocalState{appIdx: { + Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }} + cp = map[basics.AppIndex]basics.AppLocalState{appIdx: { + Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }} + b.balances[sender] = basics.AccountData{AppLocalStates: cp} + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + a.Equal(basics.TealUintType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Type) + a.Equal(basics.TealBytesType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Type) + a.Equal(uint64(1), b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Uint) + a.Equal("value", b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Bytes) +} + +func TestAppCallCreate(t *testing.T) { + a := require.New(t) + + var b testBalances + var txnCounter uint64 = 1 + ac := ApplicationCallTxnFields{} + creator := getRandomAddress(a) + // no balance record + appIdx, err := ac.createApplication(&b, creator, txnCounter) + a.Error(err) + a.Equal(basics.AppIndex(0), appIdx) + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + appIdx, err = ac.createApplication(&b, creator, txnCounter) + a.Error(err) + a.Contains(err.Error(), "max created apps per acct is") + + b.SetProto(protocol.ConsensusFuture) + ac.ApprovalProgram = []byte{1} + ac.ClearStateProgram = []byte{2} + ac.LocalStateSchema = basics.StateSchema{NumUint: 1} + ac.GlobalStateSchema = basics.StateSchema{NumByteSlice: 1} + appIdx, err = ac.createApplication(&b, creator, txnCounter) + a.NoError(err) + a.Equal(txnCounter+1, uint64(appIdx)) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + nbr, ok := b.putBalances[creator] + a.False(ok) + nbr, ok = b.putWithBalances[creator] + a.True(ok) + params, ok := nbr.AppParams[appIdx] + a.True(ok) + a.Equal(ac.ApprovalProgram, params.ApprovalProgram) + a.Equal(ac.ClearStateProgram, params.ClearStateProgram) + a.Equal(ac.LocalStateSchema, params.LocalStateSchema) + a.Equal(ac.GlobalStateSchema, params.GlobalStateSchema) + a.True(len(b.putWithNew) > 0) +} + +// TestAppCallApplyCreate carefully tracks and validates balance record updates +func TestAppCallApplyCreate(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var txnCounter uint64 = 1 + var b testBalances + + err := ac.apply(h, &b, spec, nil, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "cannot use empty ApplyData") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + var ad *ApplyData = &ApplyData{} + + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + 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) + + // error on non-existing app + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "applications that do not exist") + a.Equal(txnCounter+1, uint64(steva.appIdx)) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + + b.appCreators = map[basics.AppIndex]basics.Address{steva.appIdx: creator} + saved := b.putWithBalances[creator] + + b.ResetWrites() + + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", steva.appIdx)) + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + + b.ResetWrites() + + cp := basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + b.balances[creator] = cp + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + // 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] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.DeltaAction(4), Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + a.Equal(saved, b.balances[creator]) + saved = b.putWithBalances[creator] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + + gd = map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + ac.GlobalStateSchema = basics.StateSchema{NumUint: 1} + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "too much space: store integer") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + a.Equal(saved, b.balances[creator]) + saved = b.putWithBalances[creator] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + cp.TotalAppSchema = saved.TotalAppSchema + b.balances[creator] = cp + + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.NoError(err) + appIdx := steva.appIdx + a.Equal(uint64(appIdx), txnCounter+1) + a.Equal([]basics.Address{sender}, steva.acctWhitelist) + a.Equal([]basics.AppIndex{appIdx}, steva.appGlobalWhitelist) + a.Equal(1, b.put) + a.Equal(1, b.putWith) + a.Equal(saved, b.balances[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{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) + br = b.putWithBalances[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) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) +} + +// TestAppCallApplyCreateOptIn checks balance record fields without tracking substages +func TestAppCallApplyCreateOptIn(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + OnCompletion: OptInOC, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *ApplyData = &ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(steva.appIdx, appIdx) + br := b.balances[creator] + a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) + a.Equal([]byte{1}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppLocalStates[appIdx].Schema) + a.Equal(basics.StateSchema{NumUint: 1, NumByteSlice: 2}, br.TotalAppSchema) +} + +func TestAppCallOptIn(t *testing.T) { + a := require.New(t) + + sender := getRandomAddress(a) + + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var b testBalances + ad := basics.AccountData{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + + var params basics.AppParams + + err := applyOptIn(&b, sender, appIdx, params) + 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 = applyOptIn(&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) + + b.ResetWrites() + + ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + ad.AppLocalStates[appIdx] = basics.AppLocalState{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&b, sender, appIdx, params) + 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() + + delete(ad.AppLocalStates, appIdx) + ad.AppLocalStates[appIdx+1] = basics.AppLocalState{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&b, sender, appIdx, params) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + + b.ResetWrites() + + ad.AppLocalStates[appIdx+1] = basics.AppLocalState{ + Schema: basics.StateSchema{NumByteSlice: 1}, + } + ad.TotalAppSchema = basics.StateSchema{NumByteSlice: 1} + params.LocalStateSchema = basics.StateSchema{NumUint: 1} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&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: {Schema: basics.StateSchema{NumUint: 1}}, + appIdx + 1: {Schema: basics.StateSchema{NumByteSlice: 1}}, + }, + TotalAppSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }, + br, + ) +} + +func TestAppCallClearState(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + ac := ApplicationCallTxnFields{} + var steva testEvaluator + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var b testBalances + + ad := &ApplyData{} + b.appCreators = make(map[basics.AppIndex]basics.Address) + b.balances = make(map[basics.Address]basics.AccountData, 2) + + // check app not exist and not opted in + b.balances[sender] = basics.AccountData{} + err := ac.applyClearState(&b, sender, appIdx, ad, &steva) + a.Error(err) + a.Contains(err.Error(), "not currently opted in") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check non-existing app with empty opt-in + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, + } + err = ac.applyClearState(&b, sender, appIdx, ad, &steva) + 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) + // check original balance record not changed + br = b.balances[sender] + a.Equal(map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, br.AppLocalStates) + + b.ResetWrites() + + // check non-existing app with non-empty opt-in + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{ + appIdx: {Schema: basics.StateSchema{NumUint: 10}}, + }, + } + err = ac.applyClearState(&b, sender, appIdx, ad, &steva) + 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) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + + b.ResetWrites() + + // check existing application with failing ClearStateProgram + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdx: {ClearStateProgram: []byte{1}}, + }, + } + b.appCreators[appIdx] = creator + + steva.pass = false + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + err = ac.applyClearState(&b, sender, appIdx, ad, &steva) + 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) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + + b.ResetWrites() + + // check existing application with successful ClearStateProgram + steva.pass = true + err = ac.applyClearState(&b, sender, appIdx, ad, &steva) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + a.Equal(0, len(br.AppLocalStates)) + a.Equal(basics.StateSchema{}, br.TotalAppSchema) + a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) +} + +func TestAppCallApplyCloseOut(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: CloseOutOC, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var ad *ApplyData = &ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 closing on empty sender's balance record + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + 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) + + b.ResetWrites() + + // check a happy case + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, + } + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(2, b.put) + a.Equal(0, b.putWith) + br = b.putBalances[creator] + a.NotEqual(cbr, br) + a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + br = b.putBalances[sender] + 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) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: UpdateApplicationOC, + ApprovalProgram: []byte{2}, + ClearStateProgram: []byte{3}, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var ad *ApplyData = &ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 updating on empty sender's balance record - happy case + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + 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{3}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) +} + +func TestAppCallApplyDelete(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: DeleteApplicationOC, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var ad *ApplyData = &ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + 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) +} + +func TestAppCallApplyCreateClearState(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{2}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + OnCompletion: ClearStateOC, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *ApplyData = &ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + // check creation on empty balance record + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "not currently opted in") + a.Equal(steva.appIdx, appIdx) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + br := b.balances[creator] + a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) + a.Equal([]byte{2}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) + a.Equal(basics.TealKeyValue(nil), br.AppParams[appIdx].GlobalState) +} + +func TestAppCallApplyCreateDelete(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + OnCompletion: DeleteApplicationOC, + } + h := Header{ + Sender: sender, + } + var steva testEvaluator + var spec SpecialAddresses + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *ApplyData = &ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + // check creation on empty balance record + err := ac.apply(h, &b, spec, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(steva.appIdx, appIdx) + a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) + br := b.balances[creator] + a.Equal(basics.AppParams{}, br.AppParams[appIdx]) +} + +func TestEncodedAppTxnAllocationBounds(t *testing.T) { + // ensure that all the supported protocols have value limits less or + // equal to their corresponding codec allocbounds + for protoVer, proto := range config.Consensus { + if proto.MaxAppArgs > encodedMaxApplicationArgs { + require.Failf(t, "proto.MaxAppArgs > encodedMaxApplicationArgs", "protocol version = %s", protoVer) + } + if proto.MaxAppTxnAccounts > encodedMaxAccounts { + require.Failf(t, "proto.MaxAppTxnAccounts > encodedMaxAccounts", "protocol version = %s", protoVer) + } + if proto.MaxAppTxnForeignApps > encodedMaxForeignApps { + require.Failf(t, "proto.MaxAppTxnForeignApps > encodedMaxForeignApps", "protocol version = %s", protoVer) + } + } +} diff --git a/data/transactions/keyreg_test.go b/data/transactions/keyreg_test.go index 7998c46ba7..012be2e216 100644 --- a/data/transactions/keyreg_test.go +++ b/data/transactions/keyreg_test.go @@ -42,10 +42,18 @@ func (balances keyregTestBalances) GetAssetCreator(assetIdx basics.AssetIndex) ( return basics.Address{}, nil } +func (balances keyregTestBalances) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { + return basics.Address{}, true, nil +} + func (balances keyregTestBalances) Put(basics.BalanceRecord) error { return nil } +func (balances keyregTestBalances) PutWithCreatables(basics.BalanceRecord, []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 } @@ -54,6 +62,10 @@ func (balances keyregTestBalances) ConsensusParams() config.ConsensusParams { return config.Consensus[balances.version] } +func (balances keyregTestBalances) Round() basics.Round { + return basics.Round(4294967296) +} + func TestKeyregApply(t *testing.T) { secretSrc := keypair() src := basics.Address(secretSrc.SignatureVerifier) @@ -73,11 +85,11 @@ func TestKeyregApply(t *testing.T) { SelectionPK: vrfSecrets.PK, }, } - _, err := tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, SpecialAddresses{FeeSink: feeSink}, 0) + _, err := tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.NoError(t, err) tx.Sender = feeSink - _, err = tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, SpecialAddresses{FeeSink: feeSink}, 0) + _, err = tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.Error(t, err) tx.Sender = src @@ -86,19 +98,19 @@ func TestKeyregApply(t *testing.T) { // Going from offline to online should be okay mockBal.addrs[src] = basics.BalanceRecord{Addr: src, AccountData: basics.AccountData{Status: basics.Offline}} - _, err = tx.Apply(mockBal, SpecialAddresses{FeeSink: feeSink}, 0) + _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.NoError(t, err) // Going from online to nonparticipatory should be okay, if the protocol supports that if mockBal.ConsensusParams().SupportBecomeNonParticipatingTransactions { tx.KeyregTxnFields = KeyregTxnFields{} tx.KeyregTxnFields.Nonparticipation = true - _, err = tx.Apply(mockBal, SpecialAddresses{FeeSink: feeSink}, 0) + _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.NoError(t, err) // Nonparticipatory accounts should not be able to change status mockBal.addrs[src] = basics.BalanceRecord{Addr: src, AccountData: basics.AccountData{Status: basics.NotParticipating}} - _, err = tx.Apply(mockBal, SpecialAddresses{FeeSink: feeSink}, 0) + _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.Error(t, err) } } diff --git a/data/transactions/logic/Makefile b/data/transactions/logic/Makefile index ded134fe44..8895bc4697 100644 --- a/data/transactions/logic/Makefile +++ b/data/transactions/logic/Makefile @@ -1,7 +1,10 @@ -all: TEAL_opcodes.md wat.md +all: TEAL_opcodes.md wat.md fields_string.go -TEAL_opcodes.md: ../../../cmd/opdoc/opdoc.go eval.go assembler.go doc.go +TEAL_opcodes.md: fields_string.go ../../../cmd/opdoc/opdoc.go eval.go assembler.go doc.go opcodes.go go run ../../../cmd/opdoc/opdoc.go +fields_string.go: fields.go + go generate + wat.md: TEAL_opcodes.md README_in.md python merge.py > README.md diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 564f3ee9e4..0086579279 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -29,7 +29,16 @@ A program can either authorize some delegated action on a normal private key sig The TEAL bytecode plus the length of any Args must add up to less than 1000 bytes (consensus parameter LogicSigMaxSize). Each TEAL op has an associated cost estimate and the program cost estimate must total less than 20000 (consensus parameter LogicSigMaxCost). Most ops have an estimated cost of 1, but a few slow crypto ops are much higher. +## Execution modes +Starting from version 2 TEAL evaluator can run programs in two modes: +1. Signature verification (stateless) +2. Application run (stateful) + +Differences between modes include: +1. Max program length (consensus parameters LogicSigMaxSize, MaxApprovalProgramLen and MaxClearStateProgramLen) +2. Max program cost (consensus parameters LogicSigMaxCost, MaxAppProgramCost) +3. Opcodes availability. For example, all stateful operations are only available in stateful mode. Refer to [opcodes document](TEAL_opcodes.md) for details. ## Constants @@ -41,6 +50,30 @@ Constants are loaded into the environment by two opcodes, `intcblock` and `bytec Constants are pushed onto the stack by `intc`, `intc_[0123]`, `bytec`, and `bytec_[0123]`. The assembler will handle converting `int N` or `byte N` into the appropriate form of the instruction needed. +### Named Integer Constants + +#### OnComplete +| Value | Constant name | Description | +| --- | --- | --- | +| 0 | NoOp | Application transaction will simply call its ApprovalProgram. | +| 1 | OptIn | Application transaction will allocate some LocalState for the application in the sender's account. | +| 2 | CloseOut | Application transaction will deallocate some LocalState for the application from the user's account. | +| 3 | ClearState | Similar to CloseOut, but may never fail. This allows users to reclaim their minimum balance from an application they no longer wish to opt in to. | +| 4 | UpdateApplication | Application transaction will update the ApprovalProgram and ClearStateProgram for the application. | +| 5 | DeleteApplication | Application transaction will delete the AppParams for the application from the creator's balance. | + +#### TypeEnum constants +| Value | Constant name | Description | +| --- | --- | --- | +| 0 | unknown | Unknown type. Invalid transaction | +| 1 | pay | Payment | +| 2 | keyreg | KeyRegistration | +| 3 | acfg | AssetConfig | +| 4 | axfer | AssetTransfer | +| 5 | afrz | AssetFreeze | +| 6 | appl | ApplicationCall | + + ## Operations Most operations work with only one type of argument, uint64 or bytes, and panic if the wrong type value is on the stack. @@ -94,6 +127,9 @@ For two-argument ops, `A` is the previous element on the stack and `B` is the la | `^` | A bitwise-xor B | | `~` | bitwise invert value X | | `mulw` | A times B out to 128-bit long result as low (top) and high uint64 values on the stack | +| `concat` | pop two byte strings A and B and join them, push the result | +| `substring` | pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result | +| `substring3` | pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result | ### Loading Values @@ -122,6 +158,8 @@ Some of these have immediate data in the byte or bytes after the opcode. | `arg_3` | push Args[3] to stack | | `txn` | push field from current transaction to stack | | `gtxn` | push field to the stack from a transaction in the current transaction group | +| `txna` | push value of an array field from current transaction to stack | +| `gtxna` | push value of a field to the stack from a transaction in the current transaction group | | `global` | push value from globals to stack | | `load` | copy a value from scratch space to the stack | | `store` | pop a value from the stack and store to scratch space | @@ -133,7 +171,7 @@ Some of these have immediate data in the byte or bytes after the opcode. | 0 | Sender | []byte | 32 byte address | | 1 | Fee | uint64 | micro-Algos | | 2 | FirstValid | uint64 | round number | -| 3 | FirstValidTime | uint64 | Causes program to fail; reserved for future use. | +| 3 | FirstValidTime | uint64 | Causes program to fail; reserved for future use | | 4 | LastValid | uint64 | round number | | 5 | Note | []byte | | | 6 | Lease | []byte | | @@ -152,8 +190,16 @@ Some of these have immediate data in the byte or bytes after the opcode. | 19 | AssetSender | []byte | 32 byte address. Causes clawback of all value of asset from AssetSender if Sender is the Clawback address of the asset. | | 20 | AssetReceiver | []byte | 32 byte address | | 21 | AssetCloseTo | []byte | 32 byte address | -| 22 | GroupIndex | uint64 | Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1. | +| 22 | GroupIndex | uint64 | Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1 | | 23 | TxID | []byte | The computed ID for this transaction. 32 bytes. | +| 24 | ApplicationID | uint64 | ApplicationID from ApplicationCall transaction. LogicSigVersion >= 2. | +| 25 | OnCompletion | uint64 | ApplicationCall transaction on completion action. LogicSigVersion >= 2. | +| 26 | ApplicationArgs | []byte | Arguments passed to the application in the ApplicationCall transaction. LogicSigVersion >= 2. | +| 27 | NumAppArgs | uint64 | Number of ApplicationArgs. LogicSigVersion >= 2. | +| 28 | Accounts | []byte | Accounts listed in the ApplicationCall transaction. LogicSigVersion >= 2. | +| 29 | NumAccounts | uint64 | Number of Accounts. LogicSigVersion >= 2. | +| 30 | ApprovalProgram | []byte | Approval program. LogicSigVersion >= 2. | +| 31 | ClearStateProgram | []byte | Clear state program. LogicSigVersion >= 2. | Additional details in the [opcodes document](TEAL_opcodes.md#txn) on the `txn` op. @@ -168,8 +214,35 @@ Global fields are fields that are common to all the transactions in the group. I | 1 | MinBalance | uint64 | micro Algos | | 2 | MaxTxnLife | uint64 | rounds | | 3 | ZeroAddress | []byte | 32 byte address of all zero bytes | -| 4 | GroupSize | uint64 | Number of transactions in this atomic transaction group. At least 1. | +| 4 | GroupSize | uint64 | Number of transactions in this atomic transaction group. At least 1 | +| 5 | LogicSigVersion | uint64 | Maximum supported TEAL version. LogicSigVersion >= 2. | +| 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | +| 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | + + +**Asset Fields** + +Asset fields include `AssetHolding` and `AssetParam` fields that are used in `asset_read_*` opcodes + +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | AssetBalance | uint64 | Amount of the asset unit held by this account | +| 1 | AssetFrozen | uint64 | Is the asset frozen or not | + +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | AssetTotal | uint64 | Total number of units of this asset | +| 1 | AssetDecimals | uint64 | See AssetParams.Decimals | +| 2 | AssetDefaultFrozen | uint64 | Frozen by default or not | +| 3 | AssetUnitName | []byte | Asset unit name | +| 4 | AssetAssetName | []byte | Asset name | +| 5 | AssetURL | []byte | URL with additional info about the asset | +| 6 | AssetMetadataHash | []byte | Arbitrary commitment | +| 7 | AssetManager | []byte | Manager commitment | +| 8 | AssetReserve | []byte | Reserve address | +| 9 | AssetFreeze | []byte | Freeze address | +| 10 | AssetClawback | []byte | Clawback address | ### Flow Control @@ -178,8 +251,29 @@ Global fields are fields that are common to all the transactions in the group. I | --- | --- | | `err` | Error. Panic immediately. This is primarily a fencepost against accidental zero bytes getting compiled into programs. | | `bnz` | branch if value X is not zero | +| `bz` | branch if value X is zero | +| `b` | branch unconditionally to offset | +| `return` | use last value on stack as success value; end | | `pop` | discard value X from stack | | `dup` | duplicate last value on stack | +| `dup2` | duplicate two last values on stack: A, B -> A, B, A, B | + +### State Access + +| Op | Description | +| --- | --- | +| `balance` | get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction | +| `app_opted_in` | check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1} | +| `app_local_get` | read from account specified by Txn.Accounts[A] from local state of the current application key B => value | +| `app_local_get_ex` | read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value} | +| `app_global_get` | read key A from global state of a current application => value | +| `app_global_get_ex` | read from application A global state key B => {0 or 1 (top), value} | +| `app_local_put` | write to account specified by Txn.Accounts[A] to local state of a current application key B with value C | +| `app_global_put` | write key A and value B to global state of the current application | +| `app_local_del` | delete from account specified by Txn.Accounts[A] local state key B of the current application | +| `app_global_del` | delete key A from a global state of the current application | +| `asset_holding_get` | read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value} | +| `asset_params_get` | read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value} | # Assembler Syntax @@ -202,13 +296,15 @@ byte b32 AAAA... byte base32(AAAA...) byte b32(AAAA...) byte 0x0123456789abcdef... +byte "\x01\x02" +byte "string literal" ``` `int` constants may be `0x` prefixed for hex, `0` prefixed for octal, or decimal numbers. `intcblock` may be explictly assembled. It will conflict with the assembler gathering `int` pseudo-ops into a `intcblock` program prefix, but may be used if code only has explicit `intc` references. `intcblock` should be followed by space separated int constants all on one line. -`bytecblock` may be explicitly assembled. It will conflict with the assembler if there are any `byte` pseudo-ops but may be used if only explicit `bytec` references are used. `bytecblock` should be followed with byte constants all on one line, either 'encoding value' pairs (`b64 AAA...`) or 0x prefix or function-style values (`base64(...)`). +`bytecblock` may be explicitly assembled. It will conflict with the assembler if there are any `byte` pseudo-ops but may be used if only explicit `bytec` references are used. `bytecblock` should be followed with byte constants all on one line, either 'encoding value' pairs (`b64 AAA...`) or 0x prefix or function-style values (`base64(...)`) or string literal values. ## Labels and Branches diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md index ad71733020..5a58cb820c 100644 --- a/data/transactions/logic/README_in.md +++ b/data/transactions/logic/README_in.md @@ -29,7 +29,16 @@ A program can either authorize some delegated action on a normal private key sig The TEAL bytecode plus the length of any Args must add up to less than 1000 bytes (consensus parameter LogicSigMaxSize). Each TEAL op has an associated cost estimate and the program cost estimate must total less than 20000 (consensus parameter LogicSigMaxCost). Most ops have an estimated cost of 1, but a few slow crypto ops are much higher. +## Execution modes +Starting from version 2 TEAL evaluator can run programs in two modes: +1. Signature verification (stateless) +2. Application run (stateful) + +Differences between modes include: +1. Max program length (consensus parameters LogicSigMaxSize, MaxApprovalProgramLen and MaxClearStateProgramLen) +2. Max program cost (consensus parameters LogicSigMaxCost, MaxAppProgramCost) +3. Opcodes availability. For example, all stateful operations are only available in stateful mode. Refer to [opcodes document](TEAL_opcodes.md) for details. ## Constants @@ -41,6 +50,10 @@ Constants are loaded into the environment by two opcodes, `intcblock` and `bytec Constants are pushed onto the stack by `intc`, `intc_[0123]`, `bytec`, and `bytec_[0123]`. The assembler will handle converting `int N` or `byte N` into the appropriate form of the instruction needed. +### Named Integer Constants + +@@ named_integer_constants.md @@ + ## Operations Most operations work with only one type of argument, uint64 or bytes, and panic if the wrong type value is on the stack. @@ -88,11 +101,22 @@ Global fields are fields that are common to all the transactions in the group. I @@ global_fields.md @@ +**Asset Fields** + +Asset fields include `AssetHolding` and `AssetParam` fields that are used in `asset_read_*` opcodes + +@@ asset_holding_fields.md @@ + +@@ asset_params_fields.md @@ ### Flow Control @@ Flow_Control.md @@ +### State Access + +@@ State_Access.md @@ + # Assembler Syntax The assembler parses line by line. Ops that just use the stack appear on a line by themselves. Ops that take arguments are the op and then whitespace and then any argument or arguments. @@ -114,13 +138,15 @@ byte b32 AAAA... byte base32(AAAA...) byte b32(AAAA...) byte 0x0123456789abcdef... +byte "\x01\x02" +byte "string literal" ``` `int` constants may be `0x` prefixed for hex, `0` prefixed for octal, or decimal numbers. `intcblock` may be explictly assembled. It will conflict with the assembler gathering `int` pseudo-ops into a `intcblock` program prefix, but may be used if code only has explicit `intc` references. `intcblock` should be followed by space separated int constants all on one line. -`bytecblock` may be explicitly assembled. It will conflict with the assembler if there are any `byte` pseudo-ops but may be used if only explicit `bytec` references are used. `bytecblock` should be followed with byte constants all on one line, either 'encoding value' pairs (`b64 AAA...`) or 0x prefix or function-style values (`base64(...)`). +`bytecblock` may be explicitly assembled. It will conflict with the assembler if there are any `byte` pseudo-ops but may be used if only explicit `bytec` references are used. `bytecblock` should be followed with byte constants all on one line, either 'encoding value' pairs (`b64 AAA...`) or 0x prefix or function-style values (`base64(...)`) or string literal values. ## Labels and Branches diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 2536cb2739..928d39e70c 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -5,69 +5,76 @@ Ops have a 'cost' of 1 unless otherwise specified. ## err -- Opcode: 0x00 +- Opcode: 0x00 - Pops: _None_ - Pushes: _None_ - Error. Panic immediately. This is primarily a fencepost against accidental zero bytes getting compiled into programs. ## sha256 -- Opcode: 0x01 +- Opcode: 0x01 - Pops: *... stack*, []byte - Pushes: []byte - SHA256 hash of value X, yields [32]byte -- **Cost**: 7 +- **Cost**: + - 7 (LogicSigVersion = 1) + - 35 (LogicSigVersion = 2) ## keccak256 -- Opcode: 0x02 +- Opcode: 0x02 - Pops: *... stack*, []byte - Pushes: []byte - Keccak256 hash of value X, yields [32]byte -- **Cost**: 26 +- **Cost**: + - 26 (LogicSigVersion = 1) + - 130 (LogicSigVersion = 2) ## sha512_256 -- Opcode: 0x03 +- Opcode: 0x03 - Pops: *... stack*, []byte - Pushes: []byte - SHA512_256 hash of value X, yields [32]byte -- **Cost**: 9 +- **Cost**: + - 9 (LogicSigVersion = 1) + - 45 (LogicSigVersion = 2) ## ed25519verify -- Opcode: 0x04 +- Opcode: 0x04 - Pops: *... stack*, {[]byte A}, {[]byte B}, {[]byte C} - Pushes: uint64 - for (data A, signature B, pubkey C) verify the signature of ("ProgData" || program_hash || data) against the pubkey => {0 or 1} - **Cost**: 1900 +- Mode: Signature -The 32 byte public key is the last element on the stack, preceeded by the 64 byte signature at the second-to-last element on the stack, preceeded by the data which was signed at the third-to-last element on the stack. +The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack. ## + -- Opcode: 0x08 +- Opcode: 0x08 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A plus B. Panic on overflow. ## - -- Opcode: 0x09 +- Opcode: 0x09 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A minus B. Panic if B > A. ## / -- Opcode: 0x0a +- Opcode: 0x0a - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A divided by B. Panic if B == 0. ## * -- Opcode: 0x0b +- Opcode: 0x0b - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A times B. Panic on overflow. @@ -76,128 +83,128 @@ Overflow is an error condition which halts execution and fails the transaction. ## < -- Opcode: 0x0c +- Opcode: 0x0c - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A less than B => {0 or 1} ## > -- Opcode: 0x0d +- Opcode: 0x0d - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A greater than B => {0 or 1} ## <= -- Opcode: 0x0e +- Opcode: 0x0e - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A less than or equal to B => {0 or 1} ## >= -- Opcode: 0x0f +- Opcode: 0x0f - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A greater than or equal to B => {0 or 1} ## && -- Opcode: 0x10 +- Opcode: 0x10 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A is not zero and B is not zero => {0 or 1} ## || -- Opcode: 0x11 +- Opcode: 0x11 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A is not zero or B is not zero => {0 or 1} ## == -- Opcode: 0x12 +- Opcode: 0x12 - Pops: *... stack*, {any A}, {any B} - Pushes: uint64 - A is equal to B => {0 or 1} ## != -- Opcode: 0x13 +- Opcode: 0x13 - Pops: *... stack*, {any A}, {any B} - Pushes: uint64 - A is not equal to B => {0 or 1} ## ! -- Opcode: 0x14 +- Opcode: 0x14 - Pops: *... stack*, uint64 - Pushes: uint64 - X == 0 yields 1; else 0 ## len -- Opcode: 0x15 +- Opcode: 0x15 - Pops: *... stack*, []byte - Pushes: uint64 - yields length of byte value X ## itob -- Opcode: 0x16 +- Opcode: 0x16 - Pops: *... stack*, uint64 - Pushes: []byte - converts uint64 X to big endian bytes ## btoi -- Opcode: 0x17 +- Opcode: 0x17 - Pops: *... stack*, []byte - Pushes: uint64 - converts bytes X as big endian to uint64 -`btoi` panics if the input is longer than 8 bytes +`btoi` panics if the input is longer than 8 bytes. ## % -- Opcode: 0x18 +- Opcode: 0x18 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A modulo B. Panic if B == 0. ## | -- Opcode: 0x19 +- Opcode: 0x19 - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A bitwise-or B ## & -- Opcode: 0x1a +- Opcode: 0x1a - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A bitwise-and B ## ^ -- Opcode: 0x1b +- Opcode: 0x1b - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64 - A bitwise-xor B ## ~ -- Opcode: 0x1c +- Opcode: 0x1c - Pops: *... stack*, uint64 - Pushes: uint64 - bitwise invert value X ## mulw -- Opcode: 0x1d +- Opcode: 0x1d - Pops: *... stack*, {uint64 A}, {uint64 B} - Pushes: uint64, uint64 - A times B out to 128-bit long result as low (top) and high uint64 values on the stack @@ -220,28 +227,28 @@ Overflow is an error condition which halts execution and fails the transaction. ## intc_0 -- Opcode: 0x22 +- Opcode: 0x22 - Pops: _None_ - Pushes: uint64 - push constant 0 from intcblock to stack ## intc_1 -- Opcode: 0x23 +- Opcode: 0x23 - Pops: _None_ - Pushes: uint64 - push constant 1 from intcblock to stack ## intc_2 -- Opcode: 0x24 +- Opcode: 0x24 - Pops: _None_ - Pushes: uint64 - push constant 2 from intcblock to stack ## intc_3 -- Opcode: 0x25 +- Opcode: 0x25 - Pops: _None_ - Pushes: uint64 - push constant 3 from intcblock to stack @@ -264,28 +271,28 @@ Overflow is an error condition which halts execution and fails the transaction. ## bytec_0 -- Opcode: 0x28 +- Opcode: 0x28 - Pops: _None_ - Pushes: []byte - push constant 0 from bytecblock to stack ## bytec_1 -- Opcode: 0x29 +- Opcode: 0x29 - Pops: _None_ - Pushes: []byte - push constant 1 from bytecblock to stack ## bytec_2 -- Opcode: 0x2a +- Opcode: 0x2a - Pops: _None_ - Pushes: []byte - push constant 2 from bytecblock to stack ## bytec_3 -- Opcode: 0x2b +- Opcode: 0x2b - Pops: _None_ - Pushes: []byte - push constant 3 from bytecblock to stack @@ -296,34 +303,39 @@ Overflow is an error condition which halts execution and fails the transaction. - Pops: _None_ - Pushes: []byte - push Args[N] value to stack by index +- Mode: Signature ## arg_0 -- Opcode: 0x2d +- Opcode: 0x2d - Pops: _None_ - Pushes: []byte - push Args[0] to stack +- Mode: Signature ## arg_1 -- Opcode: 0x2e +- Opcode: 0x2e - Pops: _None_ - Pushes: []byte - push Args[1] to stack +- Mode: Signature ## arg_2 -- Opcode: 0x2f +- Opcode: 0x2f - Pops: _None_ - Pushes: []byte - push Args[2] to stack +- Mode: Signature ## arg_3 -- Opcode: 0x30 +- Opcode: 0x30 - Pops: _None_ - Pushes: []byte - push Args[3] to stack +- Mode: Signature ## txn @@ -339,7 +351,7 @@ Overflow is an error condition which halts execution and fails the transaction. | 0 | Sender | []byte | 32 byte address | | 1 | Fee | uint64 | micro-Algos | | 2 | FirstValid | uint64 | round number | -| 3 | FirstValidTime | uint64 | Causes program to fail; reserved for future use. | +| 3 | FirstValidTime | uint64 | Causes program to fail; reserved for future use | | 4 | LastValid | uint64 | round number | | 5 | Note | []byte | | | 6 | Lease | []byte | | @@ -358,20 +370,29 @@ Overflow is an error condition which halts execution and fails the transaction. | 19 | AssetSender | []byte | 32 byte address. Causes clawback of all value of asset from AssetSender if Sender is the Clawback address of the asset. | | 20 | AssetReceiver | []byte | 32 byte address | | 21 | AssetCloseTo | []byte | 32 byte address | -| 22 | GroupIndex | uint64 | Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1. | +| 22 | GroupIndex | uint64 | Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1 | | 23 | TxID | []byte | The computed ID for this transaction. 32 bytes. | +| 24 | ApplicationID | uint64 | ApplicationID from ApplicationCall transaction. LogicSigVersion >= 2. | +| 25 | OnCompletion | uint64 | ApplicationCall transaction on completion action. LogicSigVersion >= 2. | +| 26 | ApplicationArgs | []byte | Arguments passed to the application in the ApplicationCall transaction. LogicSigVersion >= 2. | +| 27 | NumAppArgs | uint64 | Number of ApplicationArgs. LogicSigVersion >= 2. | +| 28 | Accounts | []byte | Accounts listed in the ApplicationCall transaction. LogicSigVersion >= 2. | +| 29 | NumAccounts | uint64 | Number of Accounts. LogicSigVersion >= 2. | +| 30 | ApprovalProgram | []byte | Approval program. LogicSigVersion >= 2. | +| 31 | ClearStateProgram | []byte | Clear state program. LogicSigVersion >= 2. | TypeEnum mapping: | Index | "Type" string | Description | | --- | --- | --- | -| 0 | unknown | Unknown type. Invalid transaction. | +| 0 | unknown | Unknown type. Invalid transaction | | 1 | pay | Payment | | 2 | keyreg | KeyRegistration | | 3 | acfg | AssetConfig | | 4 | axfer | AssetTransfer | | 5 | afrz | AssetFreeze | +| 6 | appl | ApplicationCall | FirstValidTime causes the program to fail. The field is reserved for future use. @@ -391,7 +412,10 @@ FirstValidTime causes the program to fail. The field is reserved for future use. | 1 | MinBalance | uint64 | micro Algos | | 2 | MaxTxnLife | uint64 | rounds | | 3 | ZeroAddress | []byte | 32 byte address of all zero bytes | -| 4 | GroupSize | uint64 | Number of transactions in this atomic transaction group. At least 1. | +| 4 | GroupSize | uint64 | Number of transactions in this atomic transaction group. At least 1 | +| 5 | LogicSigVersion | uint64 | Maximum supported TEAL version. LogicSigVersion >= 2. | +| 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | +| 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | ## gtxn @@ -401,7 +425,7 @@ FirstValidTime causes the program to fail. The field is reserved for future use. - Pushes: any - push field to the stack from a transaction in the current transaction group -for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field` +for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`. ## load @@ -417,6 +441,22 @@ for notes on transaction fields available, see `txn`. If this transaction is _i_ - Pushes: _None_ - pop a value from the stack and store to scratch space +## txna + +- Opcode: 0x36 {uint8 transaction field index}{uint8 transaction field array index} +- Pops: _None_ +- Pushes: any +- push value of an array field from current transaction to stack +- LogicSigVersion >= 2 + +## gtxna + +- Opcode: 0x37 {uint8 transaction group index}{uint8 transaction field index}{uint8 transaction field array index} +- Pops: _None_ +- Pushes: any +- push value of a field to the stack from a transaction in the current transaction group +- LogicSigVersion >= 2 + ## bnz - Opcode: 0x40 {0..0x7fff forward branch offset, big endian} @@ -426,16 +466,233 @@ for notes on transaction fields available, see `txn`. If this transaction is _i_ The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping. +At LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction, removing the need for a last instruction or no-op as a branch target at the end. Branching beyond that may still fail the program. + +## bz + +- Opcode: 0x41 {0..0x7fff forward branch offset, big endian} +- Pops: *... stack*, uint64 +- Pushes: _None_ +- branch if value X is zero +- LogicSigVersion >= 2 + +See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`. + +## b + +- Opcode: 0x42 {0..0x7fff forward branch offset, big endian} +- Pops: _None_ +- Pushes: _None_ +- branch unconditionally to offset +- LogicSigVersion >= 2 + +See `bnz` for details on how branches work. `b` always jumps to the offset. + +## return + +- Opcode: 0x43 +- Pops: *... stack*, uint64 +- Pushes: _None_ +- use last value on stack as success value; end +- LogicSigVersion >= 2 + ## pop -- Opcode: 0x48 +- Opcode: 0x48 - Pops: *... stack*, any - Pushes: _None_ - discard value X from stack ## dup -- Opcode: 0x49 +- Opcode: 0x49 - Pops: *... stack*, any - Pushes: any, any - duplicate last value on stack + +## dup2 + +- Opcode: 0x4a +- Pops: *... stack*, {any A}, {any B} +- Pushes: any, any, any, any +- duplicate two last values on stack: A, B -> A, B, A, B +- LogicSigVersion >= 2 + +## concat + +- Opcode: 0x50 +- Pops: *... stack*, {[]byte A}, {[]byte B} +- Pushes: []byte +- pop two byte strings A and B and join them, push the result +- LogicSigVersion >= 2 + +`concat` panics if the result would be greater than 4096 bytes. + +## substring + +- Opcode: 0x51 {uint8 start position}{uint8 end position} +- Pops: *... stack*, []byte +- Pushes: []byte +- pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result +- LogicSigVersion >= 2 + +## substring3 + +- Opcode: 0x52 +- Pops: *... stack*, {[]byte A}, {uint64 B}, {uint64 C} +- Pushes: []byte +- pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result +- LogicSigVersion >= 2 + +## balance + +- Opcode: 0x60 +- Pops: *... stack*, uint64 +- Pushes: uint64 +- get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction +- LogicSigVersion >= 2 +- Mode: Application + +## app_opted_in + +- Opcode: 0x61 +- Pops: *... stack*, {uint64 A}, {uint64 B} +- Pushes: uint64 +- check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1} +- LogicSigVersion >= 2 +- Mode: Application + +params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise. + +## app_local_get + +- Opcode: 0x62 +- Pops: *... stack*, {uint64 A}, {[]byte B} +- Pushes: any +- read from account specified by Txn.Accounts[A] from local state of the current application key B => value +- LogicSigVersion >= 2 +- Mode: Application + +params: account index, state key. Return: value. The value is zero if the key does not exist. + +## app_local_get_ex + +- Opcode: 0x63 +- Pops: *... stack*, {uint64 A}, {uint64 B}, {[]byte C} +- Pushes: uint64, any +- read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value} +- LogicSigVersion >= 2 +- Mode: Application + +params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value. + +## app_global_get + +- Opcode: 0x64 +- Pops: *... stack*, []byte +- Pushes: any +- read key A from global state of a current application => value +- LogicSigVersion >= 2 +- Mode: Application + +params: state key. Return: value. The value is zero if the key does not exist. + +## app_global_get_ex + +- Opcode: 0x65 +- Pops: *... stack*, {uint64 A}, {[]byte B} +- Pushes: uint64, any +- read from application A global state key B => {0 or 1 (top), value} +- LogicSigVersion >= 2 +- Mode: Application + +params: application id, state key. Return: value. + +## app_local_put + +- Opcode: 0x66 +- Pops: *... stack*, {uint64 A}, {[]byte B}, {any C} +- Pushes: _None_ +- write to account specified by Txn.Accounts[A] to local state of a current application key B with value C +- LogicSigVersion >= 2 +- Mode: Application + +params: account index, state key, value. + +## app_global_put + +- Opcode: 0x67 +- Pops: *... stack*, {[]byte A}, {any B} +- Pushes: _None_ +- write key A and value B to global state of the current application +- LogicSigVersion >= 2 +- Mode: Application + +## app_local_del + +- Opcode: 0x68 +- Pops: *... stack*, {uint64 A}, {[]byte B} +- Pushes: _None_ +- delete from account specified by Txn.Accounts[A] local state key B of the current application +- LogicSigVersion >= 2 +- Mode: Application + +params: account index, state key. + +## app_global_del + +- Opcode: 0x69 +- Pops: *... stack*, []byte +- Pushes: _None_ +- delete key A from a global state of the current application +- LogicSigVersion >= 2 +- Mode: Application + +params: state key. + +## asset_holding_get + +- Opcode: 0x70 {uint8 asset holding field index} +- Pops: *... stack*, {uint64 A}, {uint64 B} +- Pushes: uint64, any +- read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value} +- LogicSigVersion >= 2 +- Mode: Application + +`asset_holding_get` Fields: + +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | AssetBalance | uint64 | Amount of the asset unit held by this account | +| 1 | AssetFrozen | uint64 | Is the asset frozen or not | + + +params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value. + +## asset_params_get + +- Opcode: 0x71 {uint8 asset params field index} +- Pops: *... stack*, {uint64 A}, {uint64 B} +- Pushes: uint64, any +- read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value} +- LogicSigVersion >= 2 +- Mode: Application + +`asset_params_get` Fields: + +| Index | Name | Type | Notes | +| --- | --- | --- | --- | +| 0 | AssetTotal | uint64 | Total number of units of this asset | +| 1 | AssetDecimals | uint64 | See AssetParams.Decimals | +| 2 | AssetDefaultFrozen | uint64 | Frozen by default or not | +| 3 | AssetUnitName | []byte | Asset unit name | +| 4 | AssetAssetName | []byte | Asset name | +| 5 | AssetURL | []byte | URL with additional info about the asset | +| 6 | AssetMetadataHash | []byte | Arbitrary commitment | +| 7 | AssetManager | []byte | Manager commitment | +| 8 | AssetReserve | []byte | Reserve address | +| 9 | AssetFreeze | []byte | Freeze address | +| 10 | AssetClawback | []byte | Clawback address | + + +params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value. diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index dd995b7c4a..989f7b91cb 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -31,7 +31,6 @@ import ( "strings" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/protocol" ) // Writer is what we want here. Satisfied by bufio.Buffer @@ -55,8 +54,12 @@ type OpStream struct { Version uint64 Trace io.Writer vubytes [9]byte - intc []uint64 - bytec [][]byte + + intc []uint64 + noIntcBlock bool + + bytec [][]byte + noBytecBlock bool // Keep a stack of the types of what we would push and pop to typecheck a program typeStack []StackType @@ -68,6 +71,17 @@ type OpStream struct { labels map[string]int labelReferences []labelReference + + // map opcode offsets to source line + offsetToLine map[int]int +} + +// GetVersion returns the LogicSigVersion we're building to +func (ops *OpStream) GetVersion() uint64 { + if ops.Version == 0 { + ops.Version = AssemblerDefaultVersion + } + return ops.Version } // SetLabelHere inserts a label reference to point to the next instruction @@ -82,6 +96,14 @@ func (ops *OpStream) SetLabelHere(label string) error { return nil } +// RecordSourceLine adds an entry to pc to line mapping +func (ops *OpStream) RecordSourceLine() { + if ops.offsetToLine == nil { + ops.offsetToLine = make(map[int]int) + } + ops.offsetToLine[ops.Out.Len()] = ops.sourceLine - 1 +} + // ReferToLabel records an opcode label refence to resolve later func (ops *OpStream) ReferToLabel(sourceLine, pc int, label string) { ops.labelReferences = append(ops.labelReferences, labelReference{sourceLine, pc, label}) @@ -124,6 +146,9 @@ func (ops *OpStream) Intc(constIndex uint) error { ops.Out.WriteByte(0x21) // intc ops.Out.WriteByte(uint8(constIndex)) } + if constIndex >= uint(len(ops.intc)) { + return fmt.Errorf("intc %d is not defined", constIndex) + } ops.tpush(StackUint64) return nil } @@ -164,6 +189,10 @@ func (ops *OpStream) Bytec(constIndex uint) error { ops.Out.WriteByte(0x27) // bytec ops.Out.WriteByte(uint8(constIndex)) } + if constIndex >= uint(len(ops.bytec)) { + return fmt.Errorf("bytec %d is not defined", constIndex) + } + ops.trace("bytec %d %s", constIndex, hex.EncodeToString(ops.bytec[constIndex])) ops.tpush(StackBytes) return nil } @@ -220,6 +249,21 @@ func (ops *OpStream) Txn(val uint64) error { return nil } +// Txna writes opcodes for loading array field from the current transaction +func (ops *OpStream) Txna(fieldNum uint64, arrayFieldIdx uint64) error { + if fieldNum >= uint64(len(TxnFieldNames)) { + return errors.New("invalid txn field") + } + if arrayFieldIdx > 255 { + return errors.New("txna cannot look up beyond index 255") + } + ops.Out.WriteByte(0x36) + ops.Out.WriteByte(uint8(fieldNum)) + ops.Out.WriteByte(uint8(arrayFieldIdx)) + ops.tpush(TxnFieldTypes[fieldNum]) + return nil +} + // Gtxn writes opcodes for loading a field from the current transaction func (ops *OpStream) Gtxn(gid, val uint64) error { if val >= uint64(len(TxnFieldNames)) { @@ -235,18 +279,59 @@ func (ops *OpStream) Gtxn(gid, val uint64) error { return nil } +// Gtxna writes opcodes for loading an array field from the current transaction +func (ops *OpStream) Gtxna(gid, fieldNum uint64, arrayFieldIdx uint64) error { + if fieldNum >= uint64(len(TxnFieldNames)) { + return errors.New("invalid txn field") + } + if gid > 255 || arrayFieldIdx > 255 { + return errors.New("gtxna cannot look up beyond index 255") + } + ops.Out.WriteByte(0x37) + ops.Out.WriteByte(uint8(gid)) + ops.Out.WriteByte(uint8(fieldNum)) + ops.Out.WriteByte(uint8(arrayFieldIdx)) + ops.tpush(TxnFieldTypes[fieldNum]) + return nil +} + // Global writes opcodes for loading an evaluator-global field func (ops *OpStream) Global(val uint64) error { if val >= uint64(len(GlobalFieldNames)) { - return errors.New("invalid txn field") + return errors.New("invalid global field") } ops.Out.WriteByte(0x32) ops.Out.WriteByte(uint8(val)) + ops.trace("%s (%s)", GlobalFieldNames[val], GlobalFieldTypes[val].String()) ops.tpush(GlobalFieldTypes[val]) return nil } -func assembleInt(ops *OpStream, args []string) error { +// AssetHolding writes opcodes for accessing data from AssetHolding +func (ops *OpStream) AssetHolding(val uint64) error { + if val >= uint64(len(AssetHoldingFieldNames)) { + return errors.New("invalid asset holding field") + } + ops.Out.WriteByte(opsByName[ops.Version]["asset_holding_get"].Opcode) + ops.Out.WriteByte(uint8(val)) + ops.tpush(AssetHoldingFieldTypes[val]) + ops.tpush(StackUint64) + return nil +} + +// AssetParams writes opcodes for accessing data from AssetParams +func (ops *OpStream) AssetParams(val uint64) error { + if val >= uint64(len(AssetParamsFieldNames)) { + return errors.New("invalid asset params field") + } + ops.Out.WriteByte(opsByName[ops.Version]["asset_params_get"].Opcode) + ops.Out.WriteByte(uint8(val)) + ops.tpush(AssetParamsFieldTypes[val]) + ops.tpush(StackUint64) + return nil +} + +func assembleInt(ops *OpStream, spec *OpSpec, args []string) error { // check friendly TypeEnum constants te, isTypeEnum := txnTypeConstToUint64[args[0]] if isTypeEnum { @@ -257,6 +342,11 @@ func assembleInt(ops *OpStream, args []string) error { if isTypeStr { return ops.Uint(uint64(tt)) } + // check OnCompetion constants + oc, isOCStr := onCompletionConstToUint64[args[0]] + if isOCStr { + return ops.Uint(uint64(oc)) + } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err @@ -265,14 +355,14 @@ func assembleInt(ops *OpStream, args []string) error { } // Explicit invocation of const lookup and push -func assembleIntC(ops *OpStream, args []string) error { +func assembleIntC(ops *OpStream, spec *OpSpec, args []string) error { constIndex, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err } return ops.Intc(uint(constIndex)) } -func assembleByteC(ops *OpStream, args []string) error { +func assembleByteC(ops *OpStream, spec *OpSpec, args []string) error { constIndex, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err @@ -345,6 +435,9 @@ func parseBinaryArgs(args []string) (val []byte, consumed int, err error) { return } consumed = 2 + } else if len(arg) > 2 && arg[0] == '"' && arg[len(arg)-1] == '"' { + val, err = parseStringLiteral(arg) + consumed = 1 } else { err = fmt.Errorf("byte arg did not parse: %v", arg) return @@ -352,10 +445,79 @@ func parseBinaryArgs(args []string) (val []byte, consumed int, err error) { return } +func parseStringLiteral(input string) (result []byte, err error) { + start := 0 + end := len(input) - 1 + if input[start] != '"' || input[end] != '"' { + return nil, fmt.Errorf("no quotes") + } + start++ + + escapeSeq := false + hexSeq := false + result = make([]byte, 0, end-start+1) + + // skip first and last quotes + pos := start + for pos < end { + char := input[pos] + if char == '\\' && !escapeSeq { + if hexSeq { + return nil, fmt.Errorf("escape seq inside hex number") + } + escapeSeq = true + pos++ + continue + } + if escapeSeq { + escapeSeq = false + switch char { + case 'n': + char = '\n' + case 'r': + char = '\r' + case 't': + char = '\t' + case '\\': + char = '\\' + case '"': + char = '"' + case 'x': + hexSeq = true + pos++ + continue + default: + return nil, fmt.Errorf("invalid escape seq \\%c", char) + } + } + if hexSeq { + hexSeq = false + if pos >= len(input)-2 { // count a closing quote + return nil, fmt.Errorf("non-terminated hex seq") + } + num, err := strconv.ParseUint(input[pos:pos+2], 16, 8) + if err != nil { + return nil, err + } + char = uint8(num) + pos++ + } + + result = append(result, char) + pos++ + } + if escapeSeq || hexSeq { + return nil, fmt.Errorf("non-terminated escape seq") + } + + return +} + // byte {base64,b64,base32,b32}(...) // byte {base64,b64,base32,b32} ... // byte 0x.... -func assembleByte(ops *OpStream, args []string) error { +// byte "this is a string\n" +func assembleByte(ops *OpStream, spec *OpSpec, args []string) error { var val []byte var err error if len(args) == 0 { @@ -368,23 +530,26 @@ func assembleByte(ops *OpStream, args []string) error { return ops.ByteLiteral(val) } -func assembleIntCBlock(ops *OpStream, args []string) error { +func assembleIntCBlock(ops *OpStream, spec *OpSpec, args []string) error { ops.Out.WriteByte(0x20) // intcblock var scratch [binary.MaxVarintLen64]byte l := binary.PutUvarint(scratch[:], uint64(len(args))) ops.Out.Write(scratch[:l]) - for _, xs := range args { + ops.intc = make([]uint64, len(args)) + for i, xs := range args { cu, err := strconv.ParseUint(xs, 0, 64) if err != nil { return err } l = binary.PutUvarint(scratch[:], cu) ops.Out.Write(scratch[:l]) + ops.intc[i] = cu } + ops.noIntcBlock = true return nil } -func assembleByteCBlock(ops *OpStream, args []string) error { +func assembleByteCBlock(ops *OpStream, spec *OpSpec, args []string) error { ops.Out.WriteByte(0x26) // bytecblock bvals := make([][]byte, 0, len(args)) rest := args @@ -404,12 +569,14 @@ func assembleByteCBlock(ops *OpStream, args []string) error { ops.Out.Write(scratch[:l]) ops.Out.Write(bv) } + ops.bytec = bvals + ops.noBytecBlock = true return nil } // addr A1EU... // parses base32-with-checksum account address strings into a byte literal -func assembleAddr(ops *OpStream, args []string) error { +func assembleAddr(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return errors.New("addr operation needs one argument") } @@ -420,7 +587,10 @@ func assembleAddr(ops *OpStream, args []string) error { return ops.ByteLiteral(addr[:]) } -func assembleArg(ops *OpStream, args []string) error { +func assembleArg(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 1 { + return errors.New("arg operation needs one argument") + } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err @@ -428,25 +598,26 @@ func assembleArg(ops *OpStream, args []string) error { return ops.Arg(val) } -func assembleBnz(ops *OpStream, args []string) error { +func assembleBranch(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { - return errors.New("bnz operation needs label argument") + return errors.New("branch operation needs label argument") } ops.ReferToLabel(ops.sourceLine, ops.Out.Len(), args[0]) - opcode := byte(0x40) - spec := opsByOpcode[opcode] - err := ops.checkArgs(spec) + err := ops.checkArgs(*spec) if err != nil { return err } - ops.Out.WriteByte(opcode) + ops.Out.WriteByte(spec.Opcode) // zero bytes will get replaced with actual offset in resolveLabels() ops.Out.WriteByte(0) ops.Out.WriteByte(0) return nil } -func assembleLoad(ops *OpStream, args []string) error { +func assembleLoad(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 1 { + return errors.New("load operation needs one argument") + } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err @@ -460,7 +631,10 @@ func assembleLoad(ops *OpStream, args []string) error { return nil } -func assembleStore(ops *OpStream, args []string) error { +func assembleStore(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 1 { + return errors.New("store operation needs one argument") + } val, err := strconv.ParseUint(args[0], 0, 64) if err != nil { return err @@ -468,142 +642,103 @@ func assembleStore(ops *OpStream, args []string) error { if val > EvalMaxScratchSize { return errors.New("store limited to 0..255") } - opcode := byte(0x35) - spec := opsByOpcode[opcode] - err = ops.checkArgs(spec) + err = ops.checkArgs(*spec) if err != nil { return err } - ops.Out.WriteByte(opcode) + ops.Out.WriteByte(spec.Opcode) ops.Out.WriteByte(byte(val)) return nil } -//go:generate stringer -type=TxnField - -// TxnField is an enum type for `txn` and `gtxn` -type TxnField int - -const ( - // Sender Transaction.Sender - Sender TxnField = iota - // Fee Transaction.Fee - Fee - // FirstValid Transaction.FirstValid - FirstValid - // FirstValidTime panic - FirstValidTime - // LastValid Transaction.LastValid - LastValid - // Note Transaction.Note - Note - // Lease Transaction.Lease - Lease - // Receiver Transaction.Receiver - Receiver - // Amount Transaction.Amount - Amount - // CloseRemainderTo Transaction.CloseRemainderTo - CloseRemainderTo - // VotePK Transaction.VotePK - VotePK - // SelectionPK Transaction.SelectionPK - SelectionPK - // VoteFirst Transaction.VoteFirst - VoteFirst - // VoteLast Transaction.VoteLast - VoteLast - // VoteKeyDilution Transaction.VoteKeyDilution - VoteKeyDilution - // Type Transaction.Type - Type - // TypeEnum int(Transaction.Type) - TypeEnum - // XferAsset Transaction.XferAsset - XferAsset - // AssetAmount Transaction.AssetAmount - AssetAmount - // AssetSender Transaction.AssetSender - AssetSender - // AssetReceiver Transaction.AssetReceiver - AssetReceiver - // AssetCloseTo Transaction.AssetCloseTo - AssetCloseTo - // GroupIndex i for txngroup[i] == Txn - GroupIndex - // TxID Transaction.ID() - TxID - - invalidTxnField // fence for some setup that loops from Sender..invalidTxnField -) +func assembleSubstring(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 2 { + return errors.New("substring expects 2 args") + } + start, err := strconv.ParseUint(args[0], 0, 64) + if err != nil { + return err + } + if start > EvalMaxScratchSize { + return errors.New("substring limited to 0..255") + } + end, err := strconv.ParseUint(args[1], 0, 64) + if err != nil { + return err + } + if end > EvalMaxScratchSize { + return errors.New("substring limited to 0..255") + } -// TxnFieldNames are arguments to the 'txn' and 'txnById' opcodes -var TxnFieldNames []string -var txnFields map[string]uint - -type txnFieldType struct { - field TxnField - ftype StackType -} - -var txnFieldTypePairs = []txnFieldType{ - {Sender, StackBytes}, - {Fee, StackUint64}, - {FirstValid, StackUint64}, - {FirstValidTime, StackUint64}, - {LastValid, StackUint64}, - {Note, StackBytes}, - {Lease, StackBytes}, - {Receiver, StackBytes}, - {Amount, StackUint64}, - {CloseRemainderTo, StackBytes}, - {VotePK, StackBytes}, - {SelectionPK, StackBytes}, - {VoteFirst, StackUint64}, - {VoteLast, StackUint64}, - {VoteKeyDilution, StackUint64}, - {Type, StackBytes}, - {TypeEnum, StackUint64}, - {XferAsset, StackUint64}, - {AssetAmount, StackUint64}, - {AssetSender, StackBytes}, - {AssetReceiver, StackBytes}, - {AssetCloseTo, StackBytes}, - {GroupIndex, StackUint64}, - {TxID, StackBytes}, -} - -// TxnFieldTypes is StackBytes or StackUint64 parallel to TxnFieldNames -var TxnFieldTypes []StackType - -// TxnTypeNames is the values of Txn.Type in enum order -var TxnTypeNames = []string{ - string(protocol.UnknownTx), - string(protocol.PaymentTx), - string(protocol.KeyRegistrationTx), - string(protocol.AssetConfigTx), - string(protocol.AssetTransferTx), - string(protocol.AssetFreezeTx), -} - -// map TxnTypeName to its enum index, for `txn TypeEnum` -var txnTypeIndexes map[string]int - -// map symbolic name to uint64 for assembleInt -var txnTypeConstToUint64 map[string]uint64 - -func assembleTxn(ops *OpStream, args []string) error { + if end < start { + return errors.New("substring end is before start") + } + opcode := byte(0x51) + err = ops.checkArgs(*spec) + if err != nil { + return err + } + ops.Out.WriteByte(opcode) + ops.Out.WriteByte(byte(start)) + ops.Out.WriteByte(byte(end)) + ops.trace(" pushes([]byte)") + ops.tpush(StackBytes) + return nil +} + +func disSubstring(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 2 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + start := uint(dis.program[dis.pc+1]) + end := uint(dis.program[dis.pc+2]) + dis.nextpc = dis.pc + 3 + _, dis.err = fmt.Fprintf(dis.out, "substring %d %d\n", start, end) +} + +func assembleTxn(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return errors.New("txn expects one argument") } - val, ok := txnFields[args[0]] - if !ok { + fs, ok := txnFieldSpecByName[args[0]] + if !ok || fs.version > ops.Version { return fmt.Errorf("txn unknown arg %s", args[0]) } + val := fs.field return ops.Txn(uint64(val)) } -func assembleGtxn(ops *OpStream, args []string) error { +// assembleTxn2 generates txn or txna opcode depending on number of operands +func assembleTxn2(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) == 1 { + return assembleTxn(ops, spec, args) + } + if len(args) == 2 { + return assembleTxna(ops, spec, args) + } + return errors.New("txn expects one or two arguments") +} + +func assembleTxna(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 2 { + return errors.New("txna expects two arguments") + } + fs, ok := txnFieldSpecByName[args[0]] + if !ok || fs.version > ops.Version || fs.field != ApplicationArgs && fs.field != Accounts { + return fmt.Errorf("txna unknown arg %s", args[0]) + } + arrayFieldIdx, err := strconv.ParseUint(args[1], 0, 64) + if err != nil { + return err + } + fieldNum := fs.field + return ops.Txna(uint64(fieldNum), uint64(arrayFieldIdx)) +} + +func assembleGtxn(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 2 { return errors.New("gtxn expects two arguments") } @@ -611,145 +746,112 @@ func assembleGtxn(ops *OpStream, args []string) error { if err != nil { return err } - val, ok := txnFields[args[1]] - if !ok { + fs, ok := txnFieldSpecByName[args[1]] + if !ok || fs.version > ops.Version { return fmt.Errorf("gtxn unknown arg %s", args[0]) } + val := fs.field return ops.Gtxn(gtid, uint64(val)) } -//go:generate stringer -type=GlobalField - -// GlobalField is an enum for `global` opcode -type GlobalField int - -const ( - // MinTxnFee ConsensusParams.MinTxnFee - MinTxnFee GlobalField = iota - // MinBalance ConsensusParams.MinBalance - MinBalance - // MaxTxnLife ConsensusParams.MaxTxnLife - MaxTxnLife - // ZeroAddress [32]byte{0...} - ZeroAddress - // GroupSize len(txn group) - GroupSize - invalidGlobalField -) - -// GlobalFieldNames are arguments to the 'global' opcode -var GlobalFieldNames []string - -type globalFieldType struct { - gfield GlobalField - ftype StackType +func assembleGtxn2(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) == 2 { + return assembleGtxn(ops, spec, args) + } + if len(args) == 3 { + return assembleGtxna(ops, spec, args) + } + return errors.New("gtxn expects two or three arguments") } -var globalFieldTypeList = []globalFieldType{ - {MinTxnFee, StackUint64}, - {MinBalance, StackUint64}, - {MaxTxnLife, StackUint64}, - {ZeroAddress, StackBytes}, - {GroupSize, StackUint64}, +func assembleGtxna(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 3 { + return errors.New("gtxna expects three arguments") + } + gtid, err := strconv.ParseUint(args[0], 0, 64) + if err != nil { + return err + } + fs, ok := txnFieldSpecByName[args[1]] + if !ok || fs.version > ops.Version || fs.field != ApplicationArgs && fs.field != Accounts { + return fmt.Errorf("gtxna unknown arg %s", args[0]) + } + arrayFieldIdx, err := strconv.ParseUint(args[2], 0, 64) + if err != nil { + return err + } + fieldNum := fs.field + return ops.Gtxna(gtid, uint64(fieldNum), uint64(arrayFieldIdx)) } -// GlobalFieldTypes is StackUint64 StackBytes in parallel with GlobalFieldNames -var GlobalFieldTypes []StackType - -var globalFields map[string]uint - -func assembleGlobal(ops *OpStream, args []string) error { +func assembleGlobal(ops *OpStream, spec *OpSpec, args []string) error { if len(args) != 1 { return errors.New("global expects one argument") } - val, ok := globalFields[args[0]] - if !ok { + fs, ok := globalFieldSpecByName[args[0]] + if !ok || fs.version > ops.Version { return fmt.Errorf("global unknown arg %v", args[0]) } + val := fs.gfield return ops.Global(uint64(val)) } -// AccountFieldNames are arguments to the 'account' opcode -var AccountFieldNames = []string{ - "Balance", -} -var accountFields map[string]uint - -// opcodesByName maps name to base opcode. Sometimes this is all we need. -var opcodesByName map[string]byte - -// argOps take an immediate value and need to parse that argument from assembler code -var argOps map[string]func(*OpStream, []string) error - -func init() { - opcodesByName = make(map[string]byte) - for _, oi := range OpSpecs { - opcodesByName[oi.Name] = oi.Opcode +func assembleAssetHolding(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 1 { + return errors.New("asset_holding_get expects one argument") } + val, ok := assetHoldingFields[args[0]] + if !ok { + return fmt.Errorf("asset_holding_get unknown arg %v", args[0]) + } + return ops.AssetHolding(uint64(val)) +} - // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() - argOps = make(map[string]func(*OpStream, []string) error) - argOps["int"] = assembleInt - argOps["intc"] = assembleIntC - argOps["intcblock"] = assembleIntCBlock - argOps["byte"] = assembleByte - argOps["bytec"] = assembleByteC - argOps["bytecblock"] = assembleByteCBlock - argOps["addr"] = assembleAddr // parse basics.Address, actually just another []byte constant - argOps["arg"] = assembleArg - argOps["txn"] = assembleTxn - argOps["gtxn"] = assembleGtxn - argOps["global"] = assembleGlobal - argOps["bnz"] = assembleBnz - argOps["load"] = assembleLoad - argOps["store"] = assembleStore - // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() - - TxnFieldNames = make([]string, int(invalidTxnField)) - for fi := Sender; fi < invalidTxnField; fi++ { - TxnFieldNames[fi] = fi.String() +func assembleAssetParams(ops *OpStream, spec *OpSpec, args []string) error { + if len(args) != 1 { + return errors.New("asset_params_get expects one argument") } - txnFields = make(map[string]uint) - for i, tfn := range TxnFieldNames { - txnFields[tfn] = uint(i) + val, ok := assetParamsFields[args[0]] + if !ok { + return fmt.Errorf("asset_params_get unknown arg %v", args[0]) } + return ops.AssetParams(uint64(val)) +} - TxnFieldTypes = make([]StackType, int(invalidTxnField)) - for i, ft := range txnFieldTypePairs { - if int(ft.field) != i { - panic("txnFieldTypePairs disjoint with TxnField enum") - } - TxnFieldTypes[i] = ft.ftype - } +type assembleFunc func(*OpStream, *OpSpec, []string) error - GlobalFieldNames = make([]string, int(invalidGlobalField)) - for i := MinTxnFee; i < invalidGlobalField; i++ { - GlobalFieldNames[int(i)] = i.String() - } - GlobalFieldTypes = make([]StackType, len(GlobalFieldNames)) - for _, ft := range globalFieldTypeList { - GlobalFieldTypes[int(ft.gfield)] = ft.ftype +func asmDefault(ops *OpStream, spec *OpSpec, args []string) error { + err := ops.checkArgs(*spec) + if err != nil { + return err } - globalFields = make(map[string]uint) - for i, gfn := range GlobalFieldNames { - globalFields[gfn] = uint(i) + if len(spec.Returns) > 0 { + ops.tpusha(spec.Returns) + ops.trace(" pushes(%s", spec.Returns[0].String()) + if len(spec.Returns) > 1 { + for _, rt := range spec.Returns[1:] { + ops.trace(", %s", rt.String()) + } + } + ops.trace(")") } - - accountFields = make(map[string]uint) - for i, gfn := range AccountFieldNames { - accountFields[gfn] = uint(i) + err = ops.Out.WriteByte(spec.Opcode) + if err != nil { + return lineErr(ops.sourceLine, err) } + return nil +} - txnTypeIndexes = make(map[string]int, len(TxnTypeNames)) - for i, tt := range TxnTypeNames { - txnTypeIndexes[tt] = i - } +// keywords handle parsing and assembling special asm language constructs like 'addr' +var keywords map[string]assembleFunc - txnTypeConstToUint64 = make(map[string]uint64, len(TxnTypeNames)) - for tt, v := range txnTypeIndexes { - symbol := TypeNameDescription(tt) - txnTypeConstToUint64[symbol] = uint64(v) - } +func init() { + // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() + keywords = make(map[string]assembleFunc) + keywords["int"] = assembleInt + keywords["byte"] = assembleByte + keywords["addr"] = assembleAddr // parse basics.Address, actually just another []byte constant + // WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() } type lineErrorWrapper struct { @@ -774,16 +876,77 @@ func typecheck(expected, got StackType) bool { return expected == got } -func filterFieldsForLineComment(fields []string) []string { - prevField := "" - for i, s := range fields { - if strings.HasPrefix(s, "//") { - if prevField != "base64" && prevField != "b64" { - return fields[:i] +var spaces = [256]uint8{'\t': 1, ' ': 1} + +func fieldsFromLine(line string) []string { + var fields []string + + i := 0 + for i < len(line) && spaces[line[i]] != 0 { + i++ + } + + start := i + inString := false + inBase64 := false + for i < len(line) { + if spaces[line[i]] == 0 { // if not space + switch line[i] { + case '"': // is a string literal? + if !inString { + if i == 0 || i > 0 && spaces[line[i-1]] != 0 { + inString = true + } + } else { + if line[i-1] != '\\' { // if not escape symbol + inString = false + } + } + case '/': // is a comment? + if i < len(line)-1 && line[i+1] == '/' && !inBase64 && !inString { + if start != i { // if a comment without whitespace + fields = append(fields, line[start:i]) + } + return fields + } + case '(': // is base64( seq? + prefix := line[start:i] + if prefix == "base64" || prefix == "b64" { + inBase64 = true + } + case ')': // is ) as base64( completion + if inBase64 { + inBase64 = false + } + default: + } + i++ + continue + } + if !inString { + field := line[start:i] + fields = append(fields, field) + if field == "base64" || field == "b64" { + inBase64 = true + } else if inBase64 { + inBase64 = false + } + } + i++ + + if !inString { + for i < len(line) && spaces[line[i]] != 0 { + i++ } + start = i } - prevField = s } + + // add rest of the string if any + if start < len(line) { + fields = append(fields, line[start:i]) + } + return fields } @@ -797,13 +960,14 @@ func (ops *OpStream) trace(format string, args ...interface{}) { // checks (and pops) arg types from arg type stack func (ops *OpStream) checkArgs(spec OpSpec) error { firstPop := true - for i, argType := range spec.Args { + for i := len(spec.Args) - 1; i >= 0; i-- { + argType := spec.Args[i] stype := ops.tpop() if firstPop { firstPop = false - ops.trace("pops(%s", stype.String()) + ops.trace("pops(%s", argType.String()) } else { - ops.trace(", %s", stype.String()) + ops.trace(", %s", argType.String()) } if !typecheck(argType, stype) { msg := fmt.Sprintf("%s arg %d wanted type %s got %s", spec.Name, i, argType.String(), stype.String()) @@ -815,7 +979,7 @@ func (ops *OpStream) checkArgs(spec OpSpec) error { } } if !firstPop { - ops.trace(") ") + ops.trace(")") } return nil } @@ -835,39 +999,34 @@ func (ops *OpStream) Assemble(fin io.Reader) error { ops.trace("%d: // line\n", ops.sourceLine) continue } - fields := strings.Fields(line) - fields = filterFieldsForLineComment(fields) + if strings.HasPrefix(line, "#pragma") { + // all pragmas must be be already processed in advance + ops.trace("%d: #pragma line\n", ops.sourceLine) + continue + } + fields := fieldsFromLine(line) if len(fields) == 0 { ops.trace("%d: no fields\n", ops.sourceLine) continue } opstring := fields[0] - argf, ok := argOps[opstring] + spec, ok := opsByName[ops.Version][opstring] + var asmFunc assembleFunc if ok { - ops.trace("%3d: %s\t", ops.sourceLine, opstring) - err := argf(ops, fields[1:]) - if err != nil { - return lineErr(ops.sourceLine, err) + asmFunc = spec.asm + } else { + kwFunc, ok := keywords[opstring] + if ok { + asmFunc = kwFunc } - ops.trace("\n") - continue } - opcode, ok := opcodesByName[opstring] - if ok { + if asmFunc != nil { ops.trace("%3d: %s\t", ops.sourceLine, opstring) - spec := opsByOpcode[opcode] - err := ops.checkArgs(spec) + ops.RecordSourceLine() + err := asmFunc(ops, &spec, fields[1:]) if err != nil { return err } - if spec.Returns != nil { - ops.tpusha(spec.Returns) - ops.trace("pushes%#v", spec.Returns) - } - err = ops.Out.WriteByte(opcode) - if err != nil { - return lineErr(ops.sourceLine, err) - } ops.trace("\n") continue } @@ -914,21 +1073,16 @@ func (ops *OpStream) resolveLabels() (err error) { } // AssemblerDefaultVersion what version of code do we emit by default -const AssemblerDefaultVersion = 1 +const AssemblerDefaultVersion = LogicVersion +const assemblerNoVersion = (^uint64(0)) // Bytes returns the finished program bytes func (ops *OpStream) Bytes() (program []byte, err error) { var scratch [binary.MaxVarintLen64]byte prebytes := bytes.Buffer{} - // TODO: configurable what version to compile for in case we're near a version boundary? - version := ops.Version - if version == 0 { - //version = config.Consensus[protocol.ConsensusCurrentVersion].LogicSigVersion - version = AssemblerDefaultVersion - } - vlen := binary.PutUvarint(scratch[:], version) + vlen := binary.PutUvarint(scratch[:], ops.GetVersion()) prebytes.Write(scratch[:vlen]) - if len(ops.intc) > 0 { + if len(ops.intc) > 0 && !ops.noIntcBlock { prebytes.WriteByte(0x20) // intcblock vlen := binary.PutUvarint(scratch[:], uint64(len(ops.intc))) prebytes.Write(scratch[:vlen]) @@ -937,7 +1091,7 @@ func (ops *OpStream) Bytes() (program []byte, err error) { prebytes.Write(scratch[:vlen]) } } - if len(ops.bytec) > 0 { + if len(ops.bytec) > 0 && !ops.noBytecBlock { prebytes.WriteByte(0x26) // bytecblock vlen := binary.PutUvarint(scratch[:], uint64(len(ops.bytec))) prebytes.Write(scratch[:vlen]) @@ -964,19 +1118,129 @@ func (ops *OpStream) Bytes() (program []byte, err error) { err = fmt.Errorf("%d program bytes but %d to buffer. err=%s", outl, ol, err) return } + + // fixup offset to line mapping + newOffsetToLine := make(map[int]int, len(ops.offsetToLine)) + for o, l := range ops.offsetToLine { + newOffsetToLine[o+pbl] = l + } + ops.offsetToLine = newOffsetToLine + program = out return } -// AssembleString takes an entire program in a string and assembles it to bytecode +// AssembleString takes an entire program in a string and assembles it to bytecode using AssemblerDefaultVersion func AssembleString(text string) ([]byte, error) { + return AssembleStringWithVersion(text, assemblerNoVersion) +} + +// AssembleStringV1 takes an entire program in a string and assembles it to bytecode using TEAL v1 +func AssembleStringV1(text string) ([]byte, error) { + return AssembleStringWithVersion(text, 1) +} + +// AssembleStringV2 takes an entire program in a string and assembles it to bytecode using TEAL v2 +func AssembleStringV2(text string) ([]byte, error) { + return AssembleStringWithVersion(text, 2) +} + +// AssembleStringWithVersion takes an entire program in a string and assembles it to bytecode using the assembler version specified +func AssembleStringWithVersion(text string, version uint64) ([]byte, error) { + program, _, err := AssembleStringWithVersionEx(text, version) + return program, err +} + +// AssembleStringWithVersionEx takes an entire program in a string and assembles it to bytecode +// using the assembler version specified. +// If version is zero it uses #pragma version or fallbacks to AssemblerDefaultVersion. +// It also returns PC to source line mapping. +func AssembleStringWithVersionEx(text string, version uint64) ([]byte, map[int]int, error) { sr := strings.NewReader(text) - ops := OpStream{} - err := ops.Assemble(sr) + ps := PragmaStream{} + err := ps.Process(sr) + if err != nil { + return nil, nil, err + } + // If version not set yet then set either default or #pragma version. + // We have to use assemblerNoVersion as a marker for non-specified version + // because version 0 is valid version for TEAL v1 + if version == assemblerNoVersion { + if ps.Version != 0 { + version = ps.Version + } else { + version = AssemblerDefaultVersion + } + } else if ps.Version != 0 && version != ps.Version { + err = fmt.Errorf("version mismatch: assembling v%d with v%d assembler", ps.Version, version) + return nil, nil, err + } else { + // otherwise the passed version matches the pragma and we are ok + } + + sr = strings.NewReader(text) + ops := OpStream{Version: version} + err = ops.Assemble(sr) if err != nil { - return nil, err + return nil, nil, err + } + program, err := ops.Bytes() + return program, ops.offsetToLine, err +} + +// PragmaStream represents all parsed pragmas from the program +type PragmaStream struct { + Version uint64 +} + +// Process all pragmas in the input stream +func (ps *PragmaStream) Process(fin io.Reader) (err error) { + scanner := bufio.NewScanner(fin) + sourceLine := 0 + for scanner.Scan() { + sourceLine++ + line := scanner.Text() + if len(line) == 0 || !strings.HasPrefix(line, "#pragma") { + continue + } + + fields := strings.Split(line, " ") + if fields[0] != "#pragma" { + err = fmt.Errorf("invalid syntax: %s", fields[0]) + return + } + if len(fields) < 2 { + err = fmt.Errorf("empty pragma") + return + } + key := fields[1] + switch key { + case "version": + if len(fields) < 3 { + err = fmt.Errorf("no version value") + return + } + value := fields[2] + var ver uint64 + if sourceLine != 1 { + err = fmt.Errorf("#pragma version is only allowed on 1st line") + return + } + ver, err = strconv.ParseUint(value, 0, 64) + if err != nil { + return + } + if ver < 1 || ver > AssemblerDefaultVersion { + err = fmt.Errorf("unsupported version: %d", ver) + return + } + ps.Version = ver + default: + err = fmt.Errorf("unsupported pragma directive: %s", key) + return + } } - return ops.Bytes() + return } type disassembleState struct { @@ -997,34 +1261,11 @@ func (dis *disassembleState) putLabel(label string, target int) { dis.pendingLabels[target] = label } -type disassembleFunc func(dis *disassembleState) +type disassembleFunc func(dis *disassembleState, spec *OpSpec) -type disassembler struct { - name string - handler disassembleFunc -} - -var disassemblers = []disassembler{ - {"intcblock", disIntcblock}, - {"intc", disIntc}, - {"bytecblock", disBytecblock}, - {"bytec", disBytec}, - {"arg", disArg}, - {"txn", disTxn}, - {"gtxn", disGtxn}, - {"global", disGlobal}, - {"bnz", disBnz}, - {"load", disLoad}, - {"store", disStore}, -} - -var disByName map[string]disassembler - -func init() { - disByName = make(map[string]disassembler) - for _, dis := range disassemblers { - disByName[dis.name] = dis - } +func disDefault(dis *disassembleState, spec *OpSpec) { + dis.nextpc = dis.pc + 1 + _, dis.err = fmt.Fprintf(dis.out, "%s\n", spec.Name) } var errTooManyIntc = errors.New("intcblock with too many items") @@ -1170,7 +1411,7 @@ func checkByteConstBlock(cx *evalContext) int { return 1 } -func disIntcblock(dis *disassembleState) { +func disIntcblock(dis *disassembleState, spec *OpSpec) { var intc []uint64 intc, dis.nextpc, dis.err = parseIntcblock(dis.program, dis.pc) if dis.err != nil { @@ -1189,12 +1430,18 @@ func disIntcblock(dis *disassembleState) { _, dis.err = dis.out.Write([]byte("\n")) } -func disIntc(dis *disassembleState) { +func disIntc(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 2 _, dis.err = fmt.Fprintf(dis.out, "intc %d\n", dis.program[dis.pc+1]) } -func disBytecblock(dis *disassembleState) { +func disBytecblock(dis *disassembleState, spec *OpSpec) { var bytec [][]byte bytec, dis.nextpc, dis.err = parseBytecBlock(dis.program, dis.pc) if dis.err != nil { @@ -1213,17 +1460,35 @@ func disBytecblock(dis *disassembleState) { _, dis.err = dis.out.Write([]byte("\n")) } -func disBytec(dis *disassembleState) { +func disBytec(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 2 _, dis.err = fmt.Fprintf(dis.out, "bytec %d\n", dis.program[dis.pc+1]) } -func disArg(dis *disassembleState) { +func disArg(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 2 _, dis.err = fmt.Fprintf(dis.out, "arg %d\n", dis.program[dis.pc+1]) } -func disTxn(dis *disassembleState) { +func disTxn(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 2 txarg := dis.program[dis.pc+1] if int(txarg) >= len(TxnFieldNames) { @@ -1233,7 +1498,30 @@ func disTxn(dis *disassembleState) { _, dis.err = fmt.Fprintf(dis.out, "txn %s\n", TxnFieldNames[txarg]) } -func disGtxn(dis *disassembleState) { +func disTxna(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 2 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + 3 + txarg := dis.program[dis.pc+1] + if int(txarg) >= len(TxnFieldNames) { + dis.err = fmt.Errorf("invalid txn arg index %d at pc=%d", txarg, dis.pc) + return + } + arrayFieldIdx := dis.program[dis.pc+2] + _, dis.err = fmt.Fprintf(dis.out, "txn %s %d\n", TxnFieldNames[txarg], arrayFieldIdx) +} + +func disGtxn(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 2 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 3 gi := dis.program[dis.pc+1] txarg := dis.program[dis.pc+2] @@ -1244,7 +1532,31 @@ func disGtxn(dis *disassembleState) { _, dis.err = fmt.Fprintf(dis.out, "gtxn %d %s\n", gi, TxnFieldNames[txarg]) } -func disGlobal(dis *disassembleState) { +func disGtxna(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 3 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + 4 + gi := dis.program[dis.pc+1] + txarg := dis.program[dis.pc+2] + if int(txarg) >= len(TxnFieldNames) { + dis.err = fmt.Errorf("invalid txn arg index %d at pc=%d", txarg, dis.pc) + return + } + arrayFieldIdx := dis.program[dis.pc+3] + _, dis.err = fmt.Fprintf(dis.out, "gtxna %d %s %d\n", gi, TxnFieldNames[txarg], arrayFieldIdx) +} + +func disGlobal(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } dis.nextpc = dis.pc + 2 garg := dis.program[dis.pc+1] if int(garg) >= len(GlobalFieldNames) { @@ -1254,7 +1566,14 @@ func disGlobal(dis *disassembleState) { _, dis.err = fmt.Fprintf(dis.out, "global %s\n", GlobalFieldNames[garg]) } -func disBnz(dis *disassembleState) { +func disBranch(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 2 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + 3 offset := (uint(dis.program[dis.pc+1]) << 8) | uint(dis.program[dis.pc+2]) target := int(offset) + dis.pc + 3 @@ -1264,30 +1583,85 @@ func disBnz(dis *disassembleState) { label = fmt.Sprintf("label%d", dis.labelCount) dis.putLabel(label, target) } - _, dis.err = fmt.Fprintf(dis.out, "bnz %s\n", label) + _, dis.err = fmt.Fprintf(dis.out, "%s %s\n", spec.Name, label) } -func disLoad(dis *disassembleState) { +func disLoad(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } n := uint(dis.program[dis.pc+1]) dis.nextpc = dis.pc + 2 _, dis.err = fmt.Fprintf(dis.out, "load %d\n", n) } -func disStore(dis *disassembleState) { +func disStore(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } n := uint(dis.program[dis.pc+1]) dis.nextpc = dis.pc + 2 _, dis.err = fmt.Fprintf(dis.out, "store %d\n", n) } -// Disassemble produces a text form of program bytes. -// AssembleString(Disassemble()) should result in the same program bytes. -func Disassemble(program []byte) (text string, err error) { +func disAssetHolding(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + 2 + arg := dis.program[dis.pc+1] + if int(arg) >= len(AssetHoldingFieldNames) { + dis.err = fmt.Errorf("invalid asset holding arg index %d at pc=%d", arg, dis.pc) + return + } + _, dis.err = fmt.Fprintf(dis.out, "asset_holding_get %s\n", AssetHoldingFieldNames[arg]) +} + +func disAssetParams(dis *disassembleState, spec *OpSpec) { + lastIdx := dis.pc + 1 + if len(dis.program) <= lastIdx { + missing := lastIdx - len(dis.program) + 1 + dis.err = fmt.Errorf("unexpected %s opcode end: missing %d bytes", spec.Name, missing) + return + } + dis.nextpc = dis.pc + 2 + arg := dis.program[dis.pc+1] + if int(arg) >= len(AssetParamsFieldNames) { + dis.err = fmt.Errorf("invalid asset params arg index %d at pc=%d", arg, dis.pc) + return + } + _, dis.err = fmt.Fprintf(dis.out, "asset_params_get %s\n", AssetParamsFieldNames[arg]) +} + +type disInfo struct { + pcOffset []PCOffset + hasStatefulOps bool +} + +// DisassembleInstrumented is like Disassemble, but additionally returns where +// each program counter value maps in the disassembly +func disassembleInstrumented(program []byte) (text string, ds disInfo, err error) { out := strings.Builder{} dis := disassembleState{program: program, out: &out} version, vlen := binary.Uvarint(program) if vlen <= 0 { fmt.Fprintf(dis.out, "// invalid version\n") - return out.String(), nil + text = out.String() + return + } + if version > LogicVersion { + fmt.Fprintf(dis.out, "// unsupported version %d\n", version) + text = out.String() + return } fmt.Fprintf(dis.out, "// version %d\n", version) dis.pc = vlen @@ -1296,11 +1670,16 @@ func Disassemble(program []byte) (text string, err error) { if hasLabel { _, dis.err = fmt.Fprintf(dis.out, "%s:\n", label) if dis.err != nil { - return "", dis.err + err = dis.err + return } } - op := opsByOpcode[program[dis.pc]] + op := opsByOpcode[version][program[dis.pc]] + if op.Modes == runModeApplication { + ds.hasStatefulOps = true + } if op.Name == "" { + ds.pcOffset = append(ds.pcOffset, PCOffset{dis.pc, out.Len()}) msg := fmt.Sprintf("invalid opcode %02x at pc=%d", program[dis.pc], dis.pc) out.WriteString(msg) out.WriteRune('\n') @@ -1308,18 +1687,31 @@ func Disassemble(program []byte) (text string, err error) { err = errors.New(msg) return } - nd, hasDis := disByName[op.Name] - if hasDis { - nd.handler(&dis) - if dis.err != nil { - return "", dis.err - } - dis.pc = dis.nextpc - continue + + // ds.pcOffset tracks where in the output each opcode maps to assembly + ds.pcOffset = append(ds.pcOffset, PCOffset{dis.pc, out.Len()}) + + // Actually do the disassembly + op.dis(&dis, &op) + if dis.err != nil { + err = dis.err + return } - out.WriteString(op.Name) - out.WriteRune('\n') - dis.pc++ + dis.pc = dis.nextpc } - return out.String(), nil + text = out.String() + return +} + +// Disassemble produces a text form of program bytes. +// AssembleString(Disassemble()) should result in the same program bytes. +func Disassemble(program []byte) (text string, err error) { + text, _, err = disassembleInstrumented(program) + return +} + +// HasStatefulOps checks if the program has stateful opcodes +func HasStatefulOps(program []byte) (bool, error) { + _, ds, err := disassembleInstrumented(program) + return ds.hasStatefulOps, err } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index f3975e94ff..b350d2801b 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -19,6 +19,7 @@ package logic import ( "bytes" "encoding/hex" + "fmt" "strings" "testing" @@ -129,7 +130,70 @@ store 2 intc 0 intc 1 mulw -pop // pop extra returned element to balance the stack +dup2 +pop +pop +pop +pop +addr RWXCBB73XJITATVQFOI7MVUUQOL2PFDDSDUMW4H4T2SNSX4SEUOQ2MM7F4 +concat +substring 42 99 +intc 0 +intc 1 +substring3 +bz there2 +b there2 +there2: +return +int 1 +balance +int 1 +app_opted_in +int 1 +byte "test" +app_local_get_ex +pop +pop +int 1 +byte "\x42\x42" +app_local_get +pop +byte 0x4242 +app_global_get +byte 0x4242 +app_global_get_ex +pop +pop +int 1 +byte 0x4242 +int 2 +app_local_put +byte 0x4242 +int 1 +app_global_put +int 0 +byte 0x4242 +app_local_del +byte 0x4242 +app_global_del +int 0 +int 1 +asset_holding_get AssetBalance +pop +pop +int 0 +int 1 +asset_params_get AssetTotal +pop +pop +txna Accounts 0 +gtxna 0 ApplicationArgs 0 +txn ApplicationID +txn OnCompletion +txn NumAppArgs +txn NumAccounts +txn ApprovalProgram +txn ClearStateProgram ` // Check that assembly output is stable across time. @@ -145,14 +209,17 @@ func TestAssemble(t *testing.T) { // Ensure that we have some basic check of all the ops, except // we don't test every combination of // intcblock,bytecblock,intc*,bytec*,arg* here. - if !strings.Contains(bigTestAssembleNonsenseProgram, spec.Name) && !strings.HasPrefix(spec.Name, "int") && !strings.HasPrefix(spec.Name, "byte") && !strings.HasPrefix(spec.Name, "arg") { + if !strings.Contains(bigTestAssembleNonsenseProgram, spec.Name) && + !strings.HasPrefix(spec.Name, "int") && + !strings.HasPrefix(spec.Name, "byte") && + !strings.HasPrefix(spec.Name, "arg") { t.Errorf("test should contain op %v", spec.Name) } } program, err := AssembleString(bigTestAssembleNonsenseProgram) require.NoError(t, err) // check that compilation is stable over time and we assemble to the same bytes this month that we did last month. - expectedBytes, _ := hex.DecodeString("012005b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f26040212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d02424200320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d48") + expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b6921072105700048482107210571004848361c0037001a0031183119311b311d311e311f") if bytes.Compare(expectedBytes, program) != 0 { // this print is for convenience if the program has been changed. the hex string can be copy pasted back in as a new expected result. t.Log(hex.EncodeToString(program)) @@ -160,41 +227,209 @@ func TestAssemble(t *testing.T) { require.Equal(t, expectedBytes, program) } -func TestOpUint(t *testing.T) { - ops := OpStream{} - err := ops.Uint(0xcafebabe) +func TestAssembleAlias(t *testing.T) { + t.Parallel() + source1 := `txn Accounts 0 // alias to txna +pop +gtxn 0 ApplicationArgs 0 // alias to gtxn +pop +` + prog1, err := AssembleString(source1) require.NoError(t, err) - program, err := ops.Bytes() + + source2 := `txna Accounts 0 +pop +gtxna 0 ApplicationArgs 0 +pop +` + prog2, err := AssembleString(source2) require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "012001bef5fad70c22", s) + + require.Equal(t, prog1, prog2) +} + +func TestAssembleTxna(t *testing.T) { + source := `txna Accounts 256` + _, err := AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txna cannot look up beyond index 255") + + source = `txna ApplicationArgs 256` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txna cannot look up beyond index 255") + + source = `txna Sender 256` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txna unknown arg") + + source = `gtxna 0 Accounts 256` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") + + source = `gtxna 0 ApplicationArgs 256` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") + + source = `gtxna 256 Accounts 0` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") + + source = `gtxna 0 Sender 256` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna unknown arg") + + source = `txn Accounts 0` + _, err = AssembleStringV1(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txn expects one argument") + + source = `txn Accounts 0 1` + _, err = AssembleStringV2(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txn expects one or two arguments") + + source = `txna Accounts 0 1` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txna expects two arguments") + + source = `txna Accounts a` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "strconv.ParseUint") + + source = `gtxn 0 Sender 0` + _, err = AssembleStringV1(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxn expects two arguments") + + source = `gtxn 0 Sender 1 2` + _, err = AssembleStringV2(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxn expects two or three arguments") + + source = `gtxna 0 Accounts 1 2` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna expects three arguments") + + source = `gtxna a Accounts 0` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "strconv.ParseUint") + + source = `gtxna 0 Accounts a` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "strconv.ParseUint") + + source = `txn ABC` + _, err = AssembleStringV2(source) + require.Error(t, err) + require.Contains(t, err.Error(), "txn unknown arg") + + source = `gtxn 0 ABC` + _, err = AssembleStringV2(source) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxn unknown arg") + + source = `gtxn a ABC` + _, err = AssembleStringV2(source) + require.Error(t, err) + require.Contains(t, err.Error(), "strconv.ParseUint") +} + +func TestAssembleGlobal(t *testing.T) { + source := `global` + _, err := AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "global expects one argument") + + source = `global a` + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "global unknown arg") +} + +func TestAssembleDefault(t *testing.T) { + source := `byte 0x1122334455 +int 1 ++ +// comment +` + _, err := AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "wanted type uint64 got []byte") +} + +// mutateProgVersion replaces version (first two symbols) in hex-encoded program +func mutateProgVersion(version uint64, prog string) string { + return fmt.Sprintf("%02x%s", version, prog[2:]) +} + +func TestOpUint(t *testing.T) { + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + ops := OpStream{Version: v} + err := ops.Uint(0xcafebabe) + require.NoError(t, err) + program, err := ops.Bytes() + require.NoError(t, err) + s := hex.EncodeToString(program) + expected := mutateProgVersion(v, "012001bef5fad70c22") + require.Equal(t, expected, s) + }) + } } func TestOpUint64(t *testing.T) { - ops := OpStream{} - err := ops.Uint(0xcafebabecafebabe) - require.NoError(t, err) - program, err := ops.Bytes() - require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "012001bef5fad7ecd7aeffca0122", s) + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + t.Parallel() + ops := OpStream{Version: v} + err := ops.Uint(0xcafebabecafebabe) + require.NoError(t, err) + program, err := ops.Bytes() + require.NoError(t, err) + s := hex.EncodeToString(program) + require.Equal(t, mutateProgVersion(v, "012001bef5fad7ecd7aeffca0122"), s) + }) + } } func TestOpBytes(t *testing.T) { - ops := OpStream{} - err := ops.ByteLiteral([]byte("abcdef")) - program, err := ops.Bytes() - require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "0126010661626364656628", s) + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + ops := OpStream{Version: v} + err := ops.ByteLiteral([]byte("abcdef")) + require.NoError(t, err) + program, err := ops.Bytes() + require.NoError(t, err) + s := hex.EncodeToString(program) + require.Equal(t, mutateProgVersion(v, "0126010661626364656628"), s) + }) + } } func TestAssembleInt(t *testing.T) { - text := "int 0xcafebabe" - program, err := AssembleString(text) - require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "012001bef5fad70c22", s) + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + text := "int 0xcafebabe" + program, err := AssembleStringWithVersion(text, v) + require.NoError(t, err) + s := hex.EncodeToString(program) + require.Equal(t, mutateProgVersion(v, "012001bef5fad70c22"), s) + }) + } } /* @@ -208,6 +443,7 @@ base64.b16encode(raw.encode()) */ func TestAssembleBytes(t *testing.T) { + t.Parallel() variations := []string{ "byte b32 MFRGGZDFMY", "byte base32 MFRGGZDFMY", @@ -222,25 +458,253 @@ func TestAssembleBytes(t *testing.T) { "byte b64(YWJjZGVm)", "byte base64(YWJjZGVm)", "byte 0x616263646566", + `byte "\x61\x62\x63\x64\x65\x66"`, + `byte "abcdef"`, } - for _, vi := range variations { - program, err := AssembleString(vi) - require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "0126010661626364656628", s) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + for _, vi := range variations { + program, err := AssembleStringWithVersion(vi, v) + require.NoError(t, err) + s := hex.EncodeToString(program) + require.Equal(t, mutateProgVersion(v, "0126010661626364656628"), s) + } + + }) + } +} + +func TestAssembleBytesString(t *testing.T) { + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + text := `byte "foo bar"` + _, err := AssembleStringWithVersion(text, v) + require.NoError(t, err) + + text = `byte "foo bar // not a comment"` + _, err = AssembleStringWithVersion(text, v) + require.NoError(t, err) + }) } } +func TestFieldsFromLine(t *testing.T) { + line := "op arg" + fields := fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "arg", fields[1]) + + line = "op arg // test" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "arg", fields[1]) + + line = "op base64 ABC//==" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC//==", fields[2]) + + line = "op base64 ABC/==" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC/==", fields[2]) + + line = "op base64 ABC/== /" + fields = fieldsFromLine(line) + require.Equal(t, 4, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC/==", fields[2]) + require.Equal(t, "/", fields[3]) + + line = "op base64 ABC/== //" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC/==", fields[2]) + + line = "op base64 ABC//== //" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC//==", fields[2]) + + line = "op b64 ABC//== //" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "b64", fields[1]) + require.Equal(t, "ABC//==", fields[2]) + + line = "op b64(ABC//==) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "b64(ABC//==)", fields[1]) + + line = "op base64(ABC//==) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64(ABC//==)", fields[1]) + + line = "op b64(ABC/==) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "b64(ABC/==)", fields[1]) + + line = "op base64(ABC/==) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64(ABC/==)", fields[1]) + + line = "base64(ABC//==)" + fields = fieldsFromLine(line) + require.Equal(t, 1, len(fields)) + require.Equal(t, "base64(ABC//==)", fields[0]) + + line = "b(ABC//==)" + fields = fieldsFromLine(line) + require.Equal(t, 1, len(fields)) + require.Equal(t, "b(ABC", fields[0]) + + line = "b(ABC//==) //" + fields = fieldsFromLine(line) + require.Equal(t, 1, len(fields)) + require.Equal(t, "b(ABC", fields[0]) + + line = "b(ABC ==) //" + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "b(ABC", fields[0]) + require.Equal(t, "==)", fields[1]) + + line = "op base64 ABC)" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC)", fields[2]) + + line = "op base64 ABC) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC)", fields[2]) + + line = "op base64 ABC//) // comment" + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, "base64", fields[1]) + require.Equal(t, "ABC//)", fields[2]) + + line = `op "test"` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test"`, fields[1]) + + line = `op "test1 test2"` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2"`, fields[1]) + + line = `op "test1 test2" // comment` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2"`, fields[1]) + + line = `op "test1 test2 // not a comment"` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2 // not a comment"`, fields[1]) + + line = `op "test1 test2 // not a comment" // comment` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2 // not a comment"`, fields[1]) + + line = `op "test1 test2 // not a comment" // comment` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2 // not a comment"`, fields[1]) + + line = `op "test1 test2" //` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2"`, fields[1]) + + line = `op "test1 test2"//` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2"`, fields[1]) + + line = `op "test1 test2` // non-terminated string literal + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2`, fields[1]) + + line = `op "test1 test2\"` // non-terminated string literal + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `"test1 test2\"`, fields[1]) + + line = `op \"test1 test2\"` // not a string literal + fields = fieldsFromLine(line) + require.Equal(t, 3, len(fields)) + require.Equal(t, "op", fields[0]) + require.Equal(t, `\"test1`, fields[1]) + require.Equal(t, `test2\"`, fields[2]) + + line = `"test1 test2"` + fields = fieldsFromLine(line) + require.Equal(t, 1, len(fields)) + require.Equal(t, `"test1 test2"`, fields[0]) + + line = `\"test1 test2"` + fields = fieldsFromLine(line) + require.Equal(t, 2, len(fields)) + require.Equal(t, `\"test1`, fields[0]) + require.Equal(t, `test2"`, fields[1]) +} + func TestAssembleRejectNegJump(t *testing.T) { + t.Parallel() text := `wat: int 1 bnz wat` - program, err := AssembleString(text) - require.Error(t, err) - require.Nil(t, program) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(text, v) + require.Error(t, err) + require.Nil(t, program) + }) + } } func TestAssembleBase64(t *testing.T) { + t.Parallel() text := `byte base64 //GWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz+0= byte base64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= // @@ -253,24 +717,48 @@ byte b64 //GWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz+8= byte b64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= == ||` - program, err := AssembleString(text) - require.NoError(t, err) - s := hex.EncodeToString(program) - require.Equal(t, "01200101260320fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfed206af19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfff20fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfef2829122210122a291211", s) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(text, v) + require.NoError(t, err) + s := hex.EncodeToString(program) + require.Equal(t, mutateProgVersion(v, "01200101260320fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfed206af19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfff20fff19644cfb2cb70426af0435cefc5613359ea8d896a2e5e76c30205d0c4cfef2829122210122a291211"), s) + }) + } } func TestAssembleRejectUnkLabel(t *testing.T) { + t.Parallel() text := `int 1 bnz nowhere` + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(text, v) + require.Error(t, err) + require.Nil(t, program) + }) + } +} + +func TestAssembleJumpToTheEnd(t *testing.T) { + text := `intcblock 1 +intc 0 +intc 0 +bnz done +done:` program, err := AssembleString(text) - require.Error(t, err) - require.Nil(t, program) + require.NoError(t, err) + require.Equal(t, 9, len(program)) + expectedProgBytes := []byte("\x01\x20\x01\x01\x22\x22\x40\x00\x00") + expectedProgBytes[0] = byte(AssemblerDefaultVersion) + require.Equal(t, expectedProgBytes, program) } func TestAssembleDisassemble(t *testing.T) { // Specifically constructed program text that should be recreated by Disassemble() // TODO: disassemble to int/byte psuedo-ops instead of raw intcblock/bytecblock/intc/bytec - text := `// version 1 + t.Parallel() + text := `// version 2 intcblock 0 1 2 3 4 5 bytecblock 0xcafed00d 0x1337 0x2001 0xdeadbeef 0x70077007 intc_1 @@ -293,6 +781,11 @@ bnz label1 global MinTxnFee global MinBalance global MaxTxnLife +global ZeroAddress +global GroupSize +global LogicSigVersion +global Round +global LatestTimestamp txn Sender txn Fee bnz label1 @@ -307,8 +800,38 @@ txn VotePK txn SelectionPK txn VoteFirst txn VoteLast +txn FirstValidTime +txn Lease +txn VoteKeyDilution +txn Type +txn TypeEnum +txn XferAsset +txn AssetAmount +txn AssetSender +txn AssetReceiver +txn AssetCloseTo +txn GroupIndex +txn TxID +txn ApplicationID +txn OnCompletion +txn ApplicationArgs +txn NumAppArgs +txn Accounts +txn NumAccounts +txn ApprovalProgram +txn ClearStateProgram gtxn 12 Fee ` + for _, globalField := range GlobalFieldNames { + if !strings.Contains(text, globalField) { + t.Errorf("TestAssembleDisassemble missing field global %v", globalField) + } + } + for _, txnField := range TxnFieldNames { + if !strings.Contains(text, txnField) { + t.Errorf("TestAssembleDisassemble missing field txn %v", txnField) + } + } program, err := AssembleString(text) require.NoError(t, err) t2, err := Disassemble(program) @@ -319,14 +842,575 @@ gtxn 12 Fee func TestAssembleDisassembleCycle(t *testing.T) { // Test that disassembly re-assembles to the same program bytes. // It disassembly won't necessarily perfectly recreate the source text, but assembling the result of Disassemble() should be the same program bytes. - program, err := AssembleString(bigTestAssembleNonsenseProgram) + t.Parallel() + + tests := map[uint64]string{ + 2: bigTestAssembleNonsenseProgram, + 1: bigTestAssembleNonsenseProgram[:strings.Index(bigTestAssembleNonsenseProgram, "dup2")], + } + + for v, source := range tests { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + t2, err := Disassemble(program) + require.NoError(t, err) + p2, err := AssembleStringV2(t2) + if err != nil { + t.Log(t2) + } + require.NoError(t, err) + require.Equal(t, program[1:], p2[1:]) + }) + } +} + +func TestAssembleDisassembleErrors(t *testing.T) { + source := `txn Sender` + program, err := AssembleString(source) require.NoError(t, err) - t2, err := Disassemble(program) + program[2] = 0x50 // txn field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn arg index") + + source = `txna Accounts 0` + program, err = AssembleString(source) + require.NoError(t, err) + program[2] = 0x50 // txn field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn arg index") + + source = `gtxn 0 Sender` + program, err = AssembleString(source) + require.NoError(t, err) + program[3] = 0x50 // txn field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn arg index") + + source = `gtxna 0 Accounts 0` + program, err = AssembleString(source) + require.NoError(t, err) + program[3] = 0x50 // txn field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn arg index") + + source = `global MinTxnFee` + program, err = AssembleString(source) + require.NoError(t, err) + program[2] = 0x50 // txn field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid global arg index") + + program[0] = 0x11 // version + out, err := Disassemble(program) + require.NoError(t, err) + require.Contains(t, out, "unsupported version") + + program[0] = 0x01 // version + program[1] = 0xFF // first opcode + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid opcode") + + source = "int 0\nint 0\nasset_holding_get AssetFrozen" + program, err = AssembleString(source) + require.NoError(t, err) + program[7] = 0x50 // holding field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid asset holding arg index") + + source = "int 0\nint 0\nasset_params_get AssetTotal" + program, err = AssembleString(source) + require.NoError(t, err) + program[7] = 0x50 // params field + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid asset params arg index") + + source = "int 0\nint 0\nasset_params_get AssetTotal" + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Disassemble(program) + require.NoError(t, err) + program = program[0 : len(program)-1] + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected asset_params_get opcode end: missing 1 bytes") + + source = "gtxna 0 Accounts 0" + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Disassemble(program) + require.NoError(t, err) + program = program[0 : len(program)-2] + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected gtxna opcode end: missing 2 bytes") + + source = "txna Accounts 0" + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Disassemble(program) + require.NoError(t, err) + program = program[0 : len(program)-1] + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected txna opcode end: missing 1 bytes") + + source = "byte 0x4141\nsubstring 0 1" + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Disassemble(program) require.NoError(t, err) - p2, err := AssembleString(t2) - if err != nil { - t.Log(t2) + program = program[0 : len(program)-1] + _, err = Disassemble(program) + require.Error(t, err) + require.Contains(t, err.Error(), "unexpected substring opcode end: missing 1 bytes") +} + +func TestAssembleVersions(t *testing.T) { + text := `int 1 +txna Accounts 0 +` + _, err := AssembleString(text) + require.NoError(t, err) + + _, err = AssembleStringV2(text) + require.NoError(t, err) + + _, err = AssembleStringV1(text) + require.Error(t, err) + require.Contains(t, err.Error(), "unknown opcode txna") +} + +func TestAssembleBalance(t *testing.T) { + t.Parallel() + + text := `byte 0x00 +balance +int 1 +==` + _, err := AssembleString(text) + require.Error(t, err) + require.Contains(t, err.Error(), "balance arg 0 wanted type uint64 got []byte") +} + +func TestAssembleAsset(t *testing.T) { + source := "int 0\nint 0\nasset_holding_get ABC 1" + _, err := AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "asset_holding_get expects one argument") + + source = "int 0\nint 0\nasset_holding_get ABC" + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "asset_holding_get unknown arg") + + source = "int 0\nint 0\nasset_params_get ABC 1" + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "asset_params_get expects one argument") + + source = "int 0\nint 0\nasset_params_get ABC" + _, err = AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), "asset_params_get unknown arg") +} + +func TestDisassembleSingleOp(t *testing.T) { + // test ensures no double arg_0 entries in disassebly listing + sample := "// version 2\narg_0\n" + program, err := AssembleString(sample) + require.NoError(t, err) + require.Equal(t, 2, len(program)) + disassembled, err := Disassemble(program) + require.NoError(t, err) + require.Equal(t, sample, disassembled) +} + +func TestAssembleOffsets(t *testing.T) { + source := "err" + program, offsets, err := AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + require.NoError(t, err) + require.Equal(t, 2, len(program)) + require.Equal(t, 1, len(offsets)) + // vlen + line, ok := offsets[0] + require.False(t, ok) + require.Equal(t, 0, line) + // err + line, ok = offsets[1] + require.True(t, ok) + require.Equal(t, 0, line) + + source = `err +// comment +err +` + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + require.NoError(t, err) + require.Equal(t, 3, len(program)) + require.Equal(t, 2, len(offsets)) + // vlen + line, ok = offsets[0] + require.False(t, ok) + require.Equal(t, 0, line) + // err 1 + line, ok = offsets[1] + require.True(t, ok) + require.Equal(t, 0, line) + // err 2 + line, ok = offsets[2] + require.True(t, ok) + require.Equal(t, 2, line) + + source = `err +bnz label1 +err +label1: +err +` + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + require.NoError(t, err) + require.Equal(t, 7, len(program)) + require.Equal(t, 4, len(offsets)) + // vlen + line, ok = offsets[0] + require.False(t, ok) + require.Equal(t, 0, line) + // err 1 + line, ok = offsets[1] + require.True(t, ok) + require.Equal(t, 0, line) + // bnz + line, ok = offsets[2] + require.True(t, ok) + require.Equal(t, 1, line) + // bnz byte 1 + line, ok = offsets[3] + require.False(t, ok) + require.Equal(t, 0, line) + // bnz byte 2 + line, ok = offsets[4] + require.False(t, ok) + require.Equal(t, 0, line) + // err 2 + line, ok = offsets[5] + require.True(t, ok) + require.Equal(t, 2, line) + // err 3 + line, ok = offsets[6] + require.True(t, ok) + require.Equal(t, 4, line) + + source = `int 0 +// comment +! +` + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + require.NoError(t, err) + require.Equal(t, 6, len(program)) + require.Equal(t, 2, len(offsets)) + // vlen + line, ok = offsets[0] + require.False(t, ok) + require.Equal(t, 0, line) + // int 0 + line, ok = offsets[4] + require.True(t, ok) + require.Equal(t, 0, line) + // ! + line, ok = offsets[5] + require.True(t, ok) + require.Equal(t, 2, line) +} + +func TestHasStatefulOps(t *testing.T) { + source := "int 1" + program, err := AssembleStringWithVersion(source, AssemblerDefaultVersion) + require.NoError(t, err) + has, err := HasStatefulOps(program) + require.NoError(t, err) + require.False(t, has) + + source = `int 1 +int 1 +app_opted_in +err +` + program, err = AssembleStringWithVersion(source, AssemblerDefaultVersion) + require.NoError(t, err) + has, err = HasStatefulOps(program) + require.NoError(t, err) + require.True(t, has) +} + +func TestStringLiteralParsing(t *testing.T) { + s := `"test"` + e := []byte(`test`) + result, err := parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"test\n"` + e = []byte(`test +`) + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"test\x0a"` + e = []byte(`test +`) + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"test\n\t\""` + e = []byte(`test + "`) + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"test 123"` + e = []byte(`test 123`) + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"\x74\x65\x73\x74\x31\x32\x33"` + e = []byte(`test123`) + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + + s = `"test` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "no quotes") + require.Nil(t, result) + + s = `test` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "no quotes") + require.Nil(t, result) + + s = `test"` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "no quotes") + require.Nil(t, result) + + s = `"test\"` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "non-terminated escape seq") + require.Nil(t, result) + + s = `"test\x\"` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "escape seq inside hex number") + require.Nil(t, result) + + s = `"test\a"` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "invalid escape seq \\a") + require.Nil(t, result) + + s = `"test\x10\x1"` + result, err = parseStringLiteral(s) + require.EqualError(t, err, "non-terminated hex seq") + require.Nil(t, result) +} + +func TestPragmaStream(t *testing.T) { + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + text := fmt.Sprintf("#pragma version %d", v) + sr := strings.NewReader(text) + ps := PragmaStream{} + err := ps.Process(sr) + require.NoError(t, err) + require.Equal(t, v, ps.Version) } + + text := `#pragma version 100` + sr := strings.NewReader(text) + ps := PragmaStream{} + err := ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported version") + require.Equal(t, uint64(0), ps.Version) + + text = `#pragma version 0` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported version") + require.Equal(t, uint64(0), ps.Version) + + text = `#pragma version a` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "strconv.ParseUint") + require.Equal(t, uint64(0), ps.Version) + + text = `#pragmas version 1` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid syntax") + require.Equal(t, uint64(0), ps.Version) + + text = ` +#pragma version a` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "allowed on 1st line") + require.Equal(t, uint64(0), ps.Version) + + text = `#pragma version 1 +#pragma version 2` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "allowed on 1st line") + require.Equal(t, uint64(1), ps.Version) + + text = `#pragma version 1 +#pragma run-mode 2` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported pragma directive: run-mode") + require.Equal(t, uint64(1), ps.Version) + + text = `#pragma versions` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported pragma directive: versions") + require.Equal(t, uint64(0), ps.Version) + + text = `# pragmas version 1` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.NoError(t, err) + require.Equal(t, uint64(0), ps.Version) + + text = ` +# pragmas version 1` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.NoError(t, err) + require.Equal(t, uint64(0), ps.Version) + + text = `#pragma` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "empty pragma") + require.Equal(t, uint64(0), ps.Version) + + text = `#pragma version` + sr = strings.NewReader(text) + ps = PragmaStream{} + err = ps.Process(sr) + require.Error(t, err) + require.Contains(t, err.Error(), "no version") + require.Equal(t, uint64(0), ps.Version) +} + +func TestAssemblePragmaVersion(t *testing.T) { + text := `#pragma version 1 +int 1 +` + program, _, err := AssembleStringWithVersionEx(text, 1) + require.NoError(t, err) + program1, err := AssembleStringV1("int 1") require.NoError(t, err) - require.Equal(t, program, p2) + require.Equal(t, program1, program) + + _, _, err = AssembleStringWithVersionEx(text, 0) + require.Error(t, err) + require.Contains(t, err.Error(), "version mismatch") + + _, _, err = AssembleStringWithVersionEx(text, 2) + require.Error(t, err) + require.Contains(t, err.Error(), "version mismatch") + + program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + require.NoError(t, err) + require.Equal(t, program1, program) + + text = `#pragma version 2 +int 1 +` + program, _, err = AssembleStringWithVersionEx(text, 2) + require.NoError(t, err) + program2, err := AssembleStringV2("int 1") + require.NoError(t, err) + require.Equal(t, program2, program) + + _, _, err = AssembleStringWithVersionEx(text, 0) + require.Error(t, err) + require.Contains(t, err.Error(), "version mismatch") + + _, _, err = AssembleStringWithVersionEx(text, 1) + require.Error(t, err) + require.Contains(t, err.Error(), "version mismatch") + + program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + require.NoError(t, err) + require.Equal(t, program2, program) + + // check if no version it defaults to the latest one + text = `byte "test" +substring 1 3 +` + program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + require.NoError(t, err) + program2, err = AssembleString(text) + require.NoError(t, err) + require.Equal(t, program2, program) + + _, err = AssembleString("#pragma unk") + require.Error(t, err) +} + +func TestAssembleConstants(t *testing.T) { + t.Parallel() + + for v := uint64(1); v <= LogicVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + _, err := AssembleStringWithVersion("intc 1", v) + require.Error(t, err) + require.Contains(t, err.Error(), "intc 1 is not defined") + + _, err = AssembleStringWithVersion("intcblock 1 2\nintc 1", v) + require.NoError(t, err) + + _, err = AssembleStringWithVersion("bytec 1", v) + require.Error(t, err) + require.Contains(t, err.Error(), "bytec 1 is not defined") + + _, err = AssembleStringWithVersion("bytecblock 0x01 0x02\nbytec 1", v) + require.NoError(t, err) + }) + } } diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go new file mode 100644 index 0000000000..05c745499d --- /dev/null +++ b/data/transactions/logic/backwardCompat_test.go @@ -0,0 +1,454 @@ +// 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 . + +package logic + +import ( + "encoding/hex" + "fmt" + "strings" + "testing" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" + "github.com/stretchr/testify/require" +) + +// This test ensures a program compiled with by pre-TEAL v2 go-algorand +// that includes all the opcodes from TEAL v1 runs in TEAL v2 runModeSignature well +var sourceTEALv1 = `byte 0x41 // A +sha256 +byte 0x559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd +== +byte 0x42 +keccak256 +byte 0x1f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a69111 +== +&& +byte 0x43 +sha512_256 +byte 0x34b99f8dde1ba273c0a28cf5b2e4dbe497f8cb2453de0c8ba6d578c9431a62cb +== +&& +arg_0 +arg_1 +arg_2 +ed25519verify +&& +// should be a single 1 on the stack +int 0 ++ +int 0 +- +int 1 +/ +int 1 +* +// should be a single 1 on the stack +int 2 +< +int 0 +> +int 1 +<= +int 1 +>= +int 1 +&& +int 0 +|| +int 1 +== +int 1 +!= +! +// should be a single 1 on the stack +arg_3 +len +int 32 +== +itob +btoi +% // 1 % 1 = 0 +int 1 +| +int 1 +& +int 0 +^ +int 0xffffffffffffffff +~ +mulw +// should be a two zeros on the stack +== +intc_0 +intc_1 +== +intc_2 +intc_3 +== +&& +intc 4 +int 1 +== +&& +pop // consume intc_N comparisons and repeat for bytec_N +bytec_0 +bytec_1 +== +bytec_2 +bytec_3 +== +&& +bytec 4 +byte 0x00 +== +&& +pop +// test all txn fields +txn Sender +txn Receiver +!= +txn Fee +txn FirstValid +== +&& +// disabled +// txn FirstValidTime +int 0 +txn LastValid +!= +&& +txn Note +txn Lease +!= +&& +txn Amount +txn GroupIndex +!= +&& +txn CloseRemainderTo +txn VotePK +== +&& +txn SelectionPK +txn Type +!= +&& +txn VoteFirst +txn VoteLast +== +&& +txn VoteKeyDilution +txn TypeEnum +!= +&& +txn XferAsset +txn AssetAmount +!= +&& +txn AssetSender +txn AssetReceiver +== +&& +txn AssetCloseTo +txn TxID +== +&& +pop +// repeat for gtxn +gtxn 0 Sender +gtxn 0 Receiver +!= +gtxn 0 Fee +gtxn 0 FirstValid +== +&& +// disabled +// gtxn 0 FirstValidTime +int 0 +gtxn 0 LastValid +!= +&& +gtxn 0 Note +gtxn 0 Lease +!= +&& +gtxn 0 Amount +gtxn 0 GroupIndex +!= +&& +gtxn 0 CloseRemainderTo +gtxn 0 VotePK +== +&& +gtxn 0 SelectionPK +gtxn 0 Type +!= +&& +gtxn 0 VoteFirst +gtxn 0 VoteLast +== +&& +gtxn 0 VoteKeyDilution +gtxn 0 TypeEnum +!= +&& +gtxn 0 XferAsset +gtxn 0 AssetAmount +!= +&& +gtxn 0 AssetSender +gtxn 0 AssetReceiver +== +&& +gtxn 0 AssetCloseTo +gtxn 0 TxID +== +&& +pop +// check global +global MinTxnFee +global MinBalance +== +global MaxTxnLife +global GroupSize +!= +&& +global ZeroAddress +byte 0x0000000000000000000000000000000000000000000000000000000000000000 +== +&& +store 0 +load 0 +&& + +// wrap up, should be a two zeros on the stack +bnz ok +err +ok: +int 1 +dup +== +` + +var programTEALv1 = "01200500010220ffffffffffffffffff012608014120559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd0142201f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a6911101432034b99f8dde1ba273c0a28cf5b2e4dbe497f8cb2453de0c8ba6d578c9431a62cb0100200000000000000000000000000000000000000000000000000000000000000000280129122a022b1210270403270512102d2e2f041022082209230a230b240c220d230e230f231022112312231314301525121617182319231a221b21041c1d12222312242512102104231210482829122a2b121027042706121048310031071331013102121022310413103105310613103108311613103109310a1210310b310f1310310c310d1210310e31101310311131121310311331141210311531171210483300003300071333000133000212102233000413103300053300061310330008330016131033000933000a121033000b33000f131033000c33000d121033000e3300101310330011330012131033001333001412103300153300171210483200320112320232041310320327071210350034001040000100234912" + +func TestBackwardCompatTEALv1(t *testing.T) { + var s crypto.Seed + crypto.RandBytes(s[:]) + c := crypto.GenerateSignatureSecrets(s) + msg := "62fdfc072182654f163f5f0f9a621d729566c74d0aa413bf009c9800418c19cd" + data, err := hex.DecodeString(msg) + require.NoError(t, err) + pk := basics.Address(c.SignatureVerifier) + + program, err := hex.DecodeString(programTEALv1) + require.NoError(t, err) + + // ensure old program is the same as a new one except TEAL version byte + program2, err := AssembleString(sourceTEALv1) + require.NoError(t, err) + require.Equal(t, program[1:], program2[1:]) + + sig := c.Sign(Msg{ + ProgramHash: crypto.HashObj(Program(program)), + Data: data[:], + }) + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{data[:], sig[:], pk[:], txn.Txn.Sender[:], txn.Txn.Note} + + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + + // ensure v1 program runs well on latest TEAL evaluator + require.Equal(t, uint8(1), program[0]) + cost, err := Check(program, ep) + require.NoError(t, err) + require.Equal(t, 2140, cost) + pass, err := Eval(program, ep) + if err != nil || !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + + cost2, err := Check(program2, ep) + require.NoError(t, err) + + // Costs for v2 should be higher because of hash opcode cost changes + require.Equal(t, 2308, cost2) + pass, err = Eval(program2, ep) + if err != nil || !pass { + t.Log(hex.EncodeToString(program2)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + + // ensure v0 program runs well on latest TEAL evaluator + program[0] = 0 + sig = c.Sign(Msg{ + ProgramHash: crypto.HashObj(Program(program)), + Data: data[:], + }) + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{data[:], sig[:], pk[:], txn.Txn.Sender[:], txn.Txn.Note} + cost, err = Check(program, ep) + require.NoError(t, err) + require.Equal(t, 2140, cost) + pass, err = Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +// ensure v2 fields error on pre TEAL v2 logicsig version +// ensure v2 fields error in v1 program +func TestBackwardCompatGlobalFields(t *testing.T) { + var fields []string + for _, fs := range globalFieldSpecs { + if fs.version > 1 { + fields = append(fields, fs.gfield.String()) + } + } + require.Greater(t, len(fields), 1) + + ledger := makeTestLedger(nil) + for _, field := range fields { + text := fmt.Sprintf("global %s", field) + // check V1 assembler fails + program, err := AssembleStringWithVersion(text, 0) + require.Error(t, err) + require.Contains(t, err.Error(), "global unknown arg") + require.Nil(t, program) + + program, err = AssembleStringWithVersion(text, 1) + require.Error(t, err) + require.Contains(t, err.Error(), "global unknown arg") + require.Nil(t, program) + + program, err = AssembleString(text) + require.NoError(t, err) + + proto := config.Consensus[protocol.ConsensusV23] + ep := defaultEvalParams(nil, nil) + ep.Proto = &proto + ep.Ledger = ledger + + // check failure with version check + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "greater than protocol supported version") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "greater than protocol supported version") + + // check opcodes failures + program[0] = 1 // set version to 1 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid global[") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid global[") + + // check opcodes failures + program[0] = 0 // set version to 0 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid global[") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid global[") + } +} + +// ensure v2 fields error in v1 program +func TestBackwardCompatTxnFields(t *testing.T) { + var fields []string + for _, fs := range txnFieldSpecs { + if fs.version > 1 { + fields = append(fields, fs.field.String()) + } + } + require.Greater(t, len(fields), 1) + + tests := []string{ + "txn %s", + "gtxn 0 %s", + } + + ledger := makeTestLedger(nil) + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + for _, field := range fields { + for _, command := range tests { + text := fmt.Sprintf(command, field) + // check V1 assembler fails + program, err := AssembleStringWithVersion(text, 0) + require.Error(t, err) + require.Contains(t, err.Error(), "txn unknown arg") + require.Nil(t, program) + + program, err = AssembleStringWithVersion(text, 1) + require.Error(t, err) + require.Contains(t, err.Error(), "txn unknown arg") + require.Nil(t, program) + + program, err = AssembleString(text) + require.NoError(t, err) + + proto := config.Consensus[protocol.ConsensusV23] + ep := defaultEvalParams(nil, nil) + ep.Proto = &proto + ep.Ledger = ledger + ep.TxnGroup = txgroup + + // check failure with version check + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "greater than protocol supported version") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "greater than protocol supported version") + + // check opcodes failures + program[0] = 1 // set version to 1 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field") + + // check opcodes failures + program[0] = 0 // set version to 0 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field") + } + } +} diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go new file mode 100644 index 0000000000..9521c176bb --- /dev/null +++ b/data/transactions/logic/debugger.go @@ -0,0 +1,281 @@ +// 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 . + +package logic + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" +) + +// DebuggerHook functions are called by eval function during TEAL program execution +// if provided +type DebuggerHook interface { + // Register is fired on program creation + Register(state *DebugState) error + // Update is fired on every step + Update(state *DebugState) error + // Complete is called when the program exits + Complete(state *DebugState) error +} + +// WebDebuggerHook represents a connection to tealdbg +type WebDebuggerHook struct { + URL string +} + +// PCOffset stores the mapping from a program counter value to an offset in the +// disassembly of the bytecode +type PCOffset struct { + PC int `codec:"pc"` + Offset int `codec:"offset"` +} + +// DebugState is a representation of the evaluation context that we encode +// to json and send to tealdbg +type DebugState struct { + // fields set once on Register + ExecID string `codec:"execid"` + Disassembly string `codec:"disasm"` + PCOffset []PCOffset `codec:"pctooffset"` + TxnGroup []transactions.SignedTxn `codec:"txngroup"` + GroupIndex int `codec:"gindex"` + Proto *config.ConsensusParams `codec:"proto"` + Globals []basics.TealValue `codec:"globals"` + + // fields updated every step + PC int `codec:"pc"` + Line int `codec:"line"` + Stack []basics.TealValue `codec:"stack"` + Scratch []basics.TealValue `codec:"scratch"` + Error string `codec:"error"` + + // global/local state changes are updated every step. Stateful TEAL only. + AppStateChage +} + +// AppStateChage encapsulates global and local app state changes +type AppStateChage struct { + GlobalStateChanges basics.StateDelta `codec:"gsch"` + LocalStateChanges map[basics.Address]basics.StateDelta `codec:"lsch"` +} + +func makeDebugState(cx *evalContext) DebugState { + disasm, dsInfo, err := disassembleInstrumented(cx.program) + if err != nil { + // Report disassembly error as program text + disasm = err.Error() + } + + hash := sha256.Sum256(cx.program) + // initialize DebuggerState with immutable fields + ds := DebugState{ + ExecID: hex.EncodeToString(hash[:]), + Disassembly: disasm, + PCOffset: dsInfo.pcOffset, + GroupIndex: cx.GroupIndex, + TxnGroup: cx.TxnGroup, + Proto: cx.Proto, + } + + globals := make([]basics.TealValue, len(GlobalFieldNames)) + for fieldIdx := range GlobalFieldNames { + sv, err := cx.globalFieldToStack(GlobalField(fieldIdx)) + if err != nil { + sv = stackValue{Bytes: []byte(err.Error())} + } + globals[fieldIdx] = stackValueToTealValue(&sv) + } + ds.Globals = globals + + // pre-allocate state maps + if (cx.runModeFlags & runModeApplication) != 0 { + ds.GlobalStateChanges = make(basics.StateDelta) + + // allocate maximum possible slots in the hashmap even if Txn.Accounts might have duplicate entries + locals := 1 + len(cx.Txn.Txn.Accounts) // sender + referenced accounts + ds.LocalStateChanges = make(map[basics.Address]basics.StateDelta, locals) + + // do not pre-allocate ds.LocalStateChanges[addr] since it initialized during update + } + + return ds +} + +// LineToPC converts line to pc +// Return 0 on unsuccess +func (d *DebugState) LineToPC(line int) int { + if len(d.PCOffset) == 0 || line < 1 { + return 0 + } + + lines := strings.Split(d.Disassembly, "\n") + if line > len(lines) { + return 0 + } + offset := len(strings.Join(lines[:line], "\n")) + + for i := 0; i < len(d.PCOffset); i++ { + if d.PCOffset[i].Offset >= offset { + return d.PCOffset[i].PC + } + } + return 0 +} + +// PCToLine converts pc to line +// Return 0 on unsuccess +func (d *DebugState) PCToLine(pc int) int { + if len(d.PCOffset) == 0 { + return 0 + } + + offset := 0 + for i := 0; i < len(d.PCOffset); i++ { + if d.PCOffset[i].PC >= pc { + offset = d.PCOffset[i].Offset + break + } + } + + one := 1 + // handle end of the program + if offset == 0 { + offset = d.PCOffset[len(d.PCOffset)-1].Offset + one = 0 + } + if offset > len(d.Disassembly) { + return 0 + } + + return len(strings.Split(d.Disassembly[:offset], "\n")) - one +} + +func stackValueToTealValue(sv *stackValue) basics.TealValue { + tv := sv.toTealValue() + return basics.TealValue{ + Type: tv.Type, + Bytes: base64.StdEncoding.EncodeToString([]byte(tv.Bytes)), + Uint: tv.Uint, + } +} + +func (cx *evalContext) refreshDebugState() *DebugState { + ds := &cx.debugState + + // Update pc, line, error, stack, and scratch space + ds.PC = cx.pc + ds.Line = ds.PCToLine(cx.pc) + if cx.err != nil { + ds.Error = cx.err.Error() + } + + stack := make([]basics.TealValue, len(cx.stack), len(cx.stack)) + for i, sv := range cx.stack { + stack[i] = stackValueToTealValue(&sv) + } + + scratch := make([]basics.TealValue, len(cx.scratch), len(cx.scratch)) + for i, sv := range cx.scratch { + scratch[i] = stackValueToTealValue(&sv) + } + + ds.Stack = stack + ds.Scratch = scratch + + if (cx.runModeFlags & runModeApplication) != 0 { + if cx.globalStateCow != nil { + for k, v := range cx.globalStateCow.delta { + ds.GlobalStateChanges[k] = v + } + } + for addr, cow := range cx.localStateCows { + delta := make(basics.StateDelta, len(cow.cow.delta)) + for k, v := range cow.cow.delta { + delta[k] = v + } + ds.LocalStateChanges[addr] = delta + } + } + + return ds +} + +func (dbg *WebDebuggerHook) postState(state *DebugState, endpoint string) error { + var body bytes.Buffer + enc := protocol.NewJSONEncoder(&body) + err := enc.Encode(state) + if err != nil { + return err + } + + u, err := url.Parse(dbg.URL) + if err != nil { + return err + } + u.Path = endpoint + + req, err := http.NewRequest(http.MethodPost, u.String(), &body) + if err != nil { + return err + } + + httpClient := &http.Client{} + r, err := httpClient.Do(req) + if err == nil { + if r.StatusCode != 200 { + err = fmt.Errorf("bad response: %d", r.StatusCode) + } + r.Body.Close() + } + return err +} + +// Register sends state to remote debugger +func (dbg *WebDebuggerHook) Register(state *DebugState) error { + u, err := url.Parse(dbg.URL) + if err != nil { + logging.Base().Errorf("Failed to parse url: %s", err.Error()) + } + h := u.Hostname() + // check for 127.0.0/8 ? + if h != "localhost" && h != "127.0.0.1" && h != "::1" { + logging.Base().Warnf("Unsecured communication with non-local debugger: %s", h) + } + return dbg.postState(state, "exec/register") +} + +// Update sends state to remote debugger +func (dbg *WebDebuggerHook) Update(state *DebugState) error { + return dbg.postState(state, "exec/update") +} + +// Complete sends state to remote debugger +func (dbg *WebDebuggerHook) Complete(state *DebugState) error { + return dbg.postState(state, "exec/complete") +} diff --git a/data/transactions/logic/debugger_test.go b/data/transactions/logic/debugger_test.go new file mode 100644 index 0000000000..8a2fba90e6 --- /dev/null +++ b/data/transactions/logic/debugger_test.go @@ -0,0 +1,127 @@ +// 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 . + +package logic + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +var testProgram string = `intcblock 0 1 1 1 1 5 100 +bytecblock 0x414c474f 0x1337 0x2001 0xdeadbeef 0x70077007 +bytec 0 +sha256 +keccak256 +sha512_256 +len +intc_0 ++ +intc_1 +- +intc_2 +/ +intc_3 +* +intc 4 +< +intc_1 +> +intc_1 +<= +intc_1 +>= +intc_1 +&& +intc_1 +|| +bytec_1 +bytec_2 +!= +bytec_3 +bytec 4 +== +&& +&& +` + +func TestWebDebuggerManual(t *testing.T) { + debugURL := os.Getenv("TEAL_DEBUGGER_URL") + if len(debugURL) == 0 { + return + } + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + txn.Lsig.Args = [][]byte{ + txn.Txn.Sender[:], + txn.Txn.Receiver[:], + txn.Txn.CloseRemainderTo[:], + txn.Txn.VotePK[:], + txn.Txn.SelectionPK[:], + txn.Txn.Note, + } + + program, err := AssembleString(testProgram) + require.NoError(t, err) + ep := defaultEvalParams(nil, &txn) + ep.TxnGroup = txgroup + ep.Debugger = &WebDebuggerHook{URL: debugURL} + _, err = Eval(program, ep) + require.NoError(t, err) +} + +type testDbgHook struct { + register int + update int + complete int + state *DebugState +} + +func (d *testDbgHook) Register(state *DebugState) error { + d.register++ + d.state = state + return nil +} + +func (d *testDbgHook) Update(state *DebugState) error { + d.update++ + d.state = state + return nil +} + +func (d *testDbgHook) Complete(state *DebugState) error { + d.complete++ + d.state = state + return nil +} + +func TestDebuggerHook(t *testing.T) { + testDbg := testDbgHook{} + program, err := AssembleString(testProgram) + require.NoError(t, err) + ep := defaultEvalParams(nil, nil) + ep.Debugger = &testDbg + _, err = Eval(program, ep) + require.NoError(t, err) + + require.Equal(t, 1, testDbg.register) + require.Equal(t, 1, testDbg.complete) + require.Greater(t, testDbg.update, 1) + require.Equal(t, 1, len(testDbg.state.Stack)) +} diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 7d51689dc4..a5e49cbbd2 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -17,6 +17,8 @@ package logic import ( + "fmt" + "github.com/algorand/go-algorand/protocol" ) @@ -81,12 +83,33 @@ var opDocList = []stringString{ {"arg_3", "push Args[3] to stack"}, {"txn", "push field from current transaction to stack"}, {"gtxn", "push field to the stack from a transaction in the current transaction group"}, + {"txna", "push value of an array field from current transaction to stack"}, + {"gtxna", "push value of a field to the stack from a transaction in the current transaction group"}, {"global", "push value from globals to stack"}, {"load", "copy a value from scratch space to the stack"}, {"store", "pop a value from the stack and store to scratch space"}, {"bnz", "branch if value X is not zero"}, + {"bz", "branch if value X is zero"}, + {"b", "branch unconditionally to offset"}, + {"return", "use last value on stack as success value; end"}, {"pop", "discard value X from stack"}, {"dup", "duplicate last value on stack"}, + {"dup2", "duplicate two last values on stack: A, B -> A, B, A, B"}, + {"concat", "pop two byte strings A and B and join them, push the result"}, + {"substring", "pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result"}, + {"substring3", "pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result"}, + {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction"}, + {"app_opted_in", "check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1}"}, + {"app_local_get", "read from account specified by Txn.Accounts[A] from local state of the current application key B => value"}, + {"app_local_get_ex", "read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value}"}, + {"app_global_get", "read key A from global state of a current application => value"}, + {"app_global_get_ex", "read from application A global state key B => {0 or 1 (top), value}"}, + {"app_local_put", "write to account specified by Txn.Accounts[A] to local state of a current application key B with value C"}, + {"app_global_put", "write key A and value B to global state of the current application"}, + {"app_local_del", "delete from account specified by Txn.Accounts[A] local state key B of the current application"}, + {"app_global_del", "delete key A from a global state of the current application"}, + {"asset_holding_get", "read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value}"}, + {"asset_params_get", "read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value}"}, } var opDocByName map[string]string @@ -108,10 +131,17 @@ var opcodeImmediateNoteList = []stringString{ {"arg", "{uint8 arg index N}"}, {"txn", "{uint8 transaction field index}"}, {"gtxn", "{uint8 transaction group index}{uint8 transaction field index}"}, + {"txna", "{uint8 transaction field index}{uint8 transaction field array index}"}, + {"gtxna", "{uint8 transaction group index}{uint8 transaction field index}{uint8 transaction field array index}"}, {"global", "{uint8 global field index}"}, {"bnz", "{0..0x7fff forward branch offset, big endian}"}, + {"bz", "{0..0x7fff forward branch offset, big endian}"}, + {"b", "{0..0x7fff forward branch offset, big endian}"}, {"load", "{uint8 position in scratch space to load from}"}, {"store", "{uint8 position in scratch space to store to}"}, + {"substring", "{uint8 start position}{uint8 end position}"}, + {"asset_holding_get", "{uint8 asset holding field index}"}, + {"asset_params_get", "{uint8 asset params field index}"}, } var opcodeImmediateNotes map[string]string @@ -125,14 +155,27 @@ func OpImmediateNote(opName string) string { // further documentation on the function of the opcode var opDocExtraList = []stringString{ - {"ed25519verify", "The 32 byte public key is the last element on the stack, preceeded by the 64 byte signature at the second-to-last element on the stack, preceeded by the data which was signed at the third-to-last element on the stack."}, - {"bnz", "The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping."}, + {"ed25519verify", "The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack."}, + {"bnz", "The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping.\n\nAt LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction, removing the need for a last instruction or no-op as a branch target at the end. Branching beyond that may still fail the program."}, + {"bz", "See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`."}, + {"b", "See `bnz` for details on how branches work. `b` always jumps to the offset."}, {"intcblock", "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script."}, {"bytecblock", "`bytecblock` loads the following program bytes into an array of byte string constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script."}, {"*", "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`."}, {"txn", "FirstValidTime causes the program to fail. The field is reserved for future use."}, - {"gtxn", "for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`"}, - {"btoi", "`btoi` panics if the input is longer than 8 bytes"}, + {"gtxn", "for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`."}, + {"btoi", "`btoi` panics if the input is longer than 8 bytes."}, + {"concat", "`concat` panics if the result would be greater than 4096 bytes."}, + {"app_opted_in", "params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise."}, + {"app_local_get", "params: account index, state key. Return: value. The value is zero if the key does not exist."}, + {"app_local_get_ex", "params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value."}, + {"app_global_get_ex", "params: application id, state key. Return: value."}, + {"app_global_get", "params: state key. Return: value. The value is zero if the key does not exist."}, + {"app_local_put", "params: account index, state key, value."}, + {"app_local_del", "params: account index, state key."}, + {"app_global_del", "params: state key."}, + {"asset_holding_get", "params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value."}, + {"asset_params_get", "params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value."}, } var opDocExtras map[string]string @@ -154,60 +197,52 @@ type OpGroup struct { // OpGroupList is groupings of ops for documentation purposes. var OpGroupList = []OpGroup{ - {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw"}}, - {"Loading Values", []string{"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "global", "load", "store"}}, - {"Flow Control", []string{"err", "bnz", "pop", "dup"}}, + {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw", "concat", "substring", "substring3"}}, + {"Loading Values", []string{"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "txna", "gtxna", "global", "load", "store"}}, + {"Flow Control", []string{"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2"}}, + {"State Access", []string{"balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get"}}, } -var opCostByName map[string]int - // OpCost returns the relative cost score for an op func OpCost(opName string) int { - if opCostByName == nil { - onn := make(map[string]int, len(opSizes)) - for _, oz := range opSizes { - if oz.cost != 1 { - onn[oz.name] = oz.cost - } + return opsByName[LogicVersion][opName].opSize.cost +} + +// OpAllCosts returns an array of the relative cost score for an op by version. +// If all the costs are the same the array is single entry +// otherwise it has costs by op version +func OpAllCosts(opName string) []int { + cost := opsByName[LogicVersion][opName].opSize.cost + costs := make([]int, LogicVersion+1) + isDifferent := false + for v := 1; v <= LogicVersion; v++ { + costs[v] = opsByName[v][opName].opSize.cost + if costs[v] > 0 && costs[v] != cost { + isDifferent = true } - opCostByName = onn } - cost, hit := opCostByName[opName] - if hit { - return cost + if !isDifferent { + return []int{cost} } - return 1 -} -var opSizeByName map[string]int + return costs +} // OpSize returns the number of bytes for an op. 0 for variable. func OpSize(opName string) int { - if opSizeByName == nil { - onn := make(map[string]int, len(opSizes)) - for _, oz := range opSizes { - if oz.size != 1 { - onn[oz.name] = oz.size - } - } - opSizeByName = onn - } - cost, hit := opSizeByName[opName] - if hit { - return cost - } - return 1 + return opsByName[LogicVersion][opName].opSize.size } // see assembler.go TxnTypeNames // also used to parse symbolic constants for `int` var typeEnumDescriptions = []stringString{ - {string(protocol.UnknownTx), "Unknown type. Invalid transaction."}, + {string(protocol.UnknownTx), "Unknown type. Invalid transaction"}, {string(protocol.PaymentTx), "Payment"}, {string(protocol.KeyRegistrationTx), "KeyRegistration"}, {string(protocol.AssetConfigTx), "AssetConfig"}, {string(protocol.AssetTransferTx), "AssetTransfer"}, {string(protocol.AssetFreezeTx), "AssetFreeze"}, + {string(protocol.ApplicationCallTx), "ApplicationCall"}, } // TypeNameDescription returns extra description about a low level protocol transaction Type string @@ -220,11 +255,31 @@ func TypeNameDescription(typeName string) string { return "invalid type name" } +// see assembler.go TxnTypeNames +// also used to parse symbolic constants for `int` +var onCompletionDescriptions = map[OnCompletionConstType]string{ + NoOp: "Application transaction will simply call its ApprovalProgram.", + OptIn: "Application transaction will allocate some LocalState for the application in the sender's account.", + CloseOut: "Application transaction will deallocate some LocalState for the application from the user's account.", + ClearState: "Similar to CloseOut, but may never fail. This allows users to reclaim their minimum balance from an application they no longer wish to opt in to.", + UpdateApplication: "Application transaction will update the ApprovalProgram and ClearStateProgram for the application.", + DeleteApplication: "Application transaction will delete the AppParams for the application from the creator's balance.", +} + +// OnCompletionDescription returns extra description about OnCompletion constants +func OnCompletionDescription(value uint64) string { + desc, ok := onCompletionDescriptions[OnCompletionConstType(value)] + if ok { + return desc + } + return "invalid constant value" +} + var txnFieldDocList = []stringString{ {"Sender", "32 byte address"}, {"Fee", "micro-Algos"}, {"FirstValid", "round number"}, - {"FirstValidTime", "Causes program to fail; reserved for future use."}, + {"FirstValidTime", "Causes program to fail; reserved for future use"}, {"LastValid", "round number"}, {"Receiver", "32 byte address"}, {"Amount", "micro-Algos"}, @@ -239,25 +294,96 @@ var txnFieldDocList = []stringString{ {"AssetSender", "32 byte address. Causes clawback of all value of asset from AssetSender if Sender is the Clawback address of the asset."}, {"AssetReceiver", "32 byte address"}, {"AssetCloseTo", "32 byte address"}, - {"GroupIndex", "Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1."}, + {"GroupIndex", "Position of this transaction within an atomic transaction group. A stand-alone transaction is implicitly element 0 in a group of 1"}, {"TxID", "The computed ID for this transaction. 32 bytes."}, + {"ApplicationID", "ApplicationID from ApplicationCall transaction"}, + {"OnCompletion", "ApplicationCall transaction on completion action"}, + {"ApplicationArgs", "Arguments passed to the application in the ApplicationCall transaction"}, + {"NumAppArgs", "Number of ApplicationArgs"}, + {"Accounts", "Accounts listed in the ApplicationCall transaction"}, + {"NumAccounts", "Number of Accounts"}, + {"ApprovalProgram", "Approval program"}, + {"ClearStateProgram", "Clear state program"}, } // TxnFieldDocs are notes on fields available by `txn` and `gtxn` -var TxnFieldDocs map[string]string +var txnFieldDocs map[string]string + +// TxnFieldDocs are notes on fields available by `txn` and `gtxn` with extra versioning info if any +func TxnFieldDocs() map[string]string { + return fieldsDocWithExtra(txnFieldDocs, txnFieldSpecByName) +} var globalFieldDocList = []stringString{ {"MinTxnFee", "micro Algos"}, {"MinBalance", "micro Algos"}, {"MaxTxnLife", "rounds"}, {"ZeroAddress", "32 byte address of all zero bytes"}, - {"GroupSize", "Number of transactions in this atomic transaction group. At least 1."}, + {"GroupSize", "Number of transactions in this atomic transaction group. At least 1"}, + {"LogicSigVersion", "Maximum supported TEAL version"}, + {"Round", "Current round number"}, + {"LatestTimestamp", "Last confirmed block UNIX timestamp. Fails if negative"}, +} + +// globalFieldDocs are notes on fields available in `global` +var globalFieldDocs map[string]string + +// GlobalFieldDocs are notes on fields available in `global` with extra versioning info if any +func GlobalFieldDocs() map[string]string { + return fieldsDocWithExtra(globalFieldDocs, globalFieldSpecByName) +} + +type extractor interface { + getExtraFor(string) string +} + +func fieldsDocWithExtra(source map[string]string, ex extractor) map[string]string { + result := make(map[string]string, len(source)) + for name, doc := range source { + if extra := ex.getExtraFor(name); len(extra) > 0 { + if len(doc) == 0 { + doc = extra + } else { + sep := ". " + if doc[len(doc)-1] == '.' { + sep = " " + } + doc = fmt.Sprintf("%s%s%s", doc, sep, extra) + } + } + result[name] = doc + } + return result +} + +var assetHoldingFieldDocList = []stringString{ + {"AssetBalance", "Amount of the asset unit held by this account"}, + {"AssetFrozen", "Is the asset frozen or not"}, +} + +// AssetHoldingFieldDocs are notes on fields available in `asset_holding_get` +var AssetHoldingFieldDocs map[string]string + +var assetParamsFieldDocList = []stringString{ + {"AssetTotal", "Total number of units of this asset"}, + {"AssetDecimals", "See AssetParams.Decimals"}, + {"AssetDefaultFrozen", "Frozen by default or not"}, + {"AssetUnitName", "Asset unit name"}, + {"AssetAssetName", "Asset name"}, + {"AssetURL", "URL with additional info about the asset"}, + {"AssetMetadataHash", "Arbitrary commitment"}, + {"AssetManager", "Manager commitment"}, + {"AssetReserve", "Reserve address"}, + {"AssetFreeze", "Freeze address"}, + {"AssetClawback", "Clawback address"}, } -// GlobalFieldDocs are notes on fields available in `global` -var GlobalFieldDocs map[string]string +// AssetParamsFieldDocs are notes on fields available in `asset_params_get` +var AssetParamsFieldDocs map[string]string func init() { - TxnFieldDocs = stringStringListToMap(txnFieldDocList) - GlobalFieldDocs = stringStringListToMap(globalFieldDocList) + txnFieldDocs = stringStringListToMap(txnFieldDocList) + globalFieldDocs = stringStringListToMap(globalFieldDocList) + AssetHoldingFieldDocs = stringStringListToMap(assetHoldingFieldDocList) + AssetParamsFieldDocs = stringStringListToMap(assetParamsFieldDocList) } diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index 72abd8fdf3..6031df10c1 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -89,6 +89,16 @@ func TestOpCost(t *testing.T) { require.Equal(t, 1, c) c = OpCost("sha256") require.True(t, c > 1) + + a := OpAllCosts("+") + require.Equal(t, 1, len(a)) + require.Equal(t, 1, a[0]) + + a = OpAllCosts("sha256") + require.True(t, len(a) > 1) + for v := 1; v <= LogicVersion; v++ { + require.True(t, a[v] > 1) + } } func TestOpSize(t *testing.T) { @@ -106,3 +116,26 @@ func TestTypeNameDescription(t *testing.T) { } require.Equal(t, "invalid type name", TypeNameDescription("invalid type name")) } + +func TestOnCompletionDescription(t *testing.T) { + desc := OnCompletionDescription(0) + require.Equal(t, "Application transaction will simply call its ApprovalProgram.", desc) + + desc = OnCompletionDescription(100) + require.Equal(t, "invalid constant value", desc) +} + +func TestFieldDocs(t *testing.T) { + txnFields := TxnFieldDocs() + require.Greater(t, len(txnFields), 0) + + globalFields := GlobalFieldDocs() + require.Greater(t, len(globalFields), 0) + + doc := globalFields["MinTxnFee"] + require.NotContains(t, doc, "LogicSigVersion >= 2") + + doc = globalFields["Round"] + require.Contains(t, doc, "LogicSigVersion >= 2") + +} diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index a6b6fea369..5e3d080025 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -42,11 +42,14 @@ import ( ) // EvalMaxVersion is the max version we can interpret and run -const EvalMaxVersion = 1 +const EvalMaxVersion = LogicVersion // EvalMaxScratchSize is the maximum number of scratch slots. const EvalMaxScratchSize = 255 +// MaxStringSize is the limit of byte strings created by `concat` +const MaxStringSize = 4096 + // stackValue is the type for the operand stack. // Each stackValue is either a valid []byte value or a uint64 value. // If (.Bytes != nil) the stackValue is a []byte value, otherwise uint64 value. @@ -76,6 +79,37 @@ func (sv *stackValue) String() string { return fmt.Sprintf("%d 0x%x", sv.Uint, sv.Uint) } +func stackValueFromTealValue(tv *basics.TealValue) (sv stackValue, err error) { + switch tv.Type { + case basics.TealBytesType: + sv.Bytes = []byte(tv.Bytes) + case basics.TealUintType: + sv.Uint = tv.Uint + default: + err = fmt.Errorf("invalid TealValue type: %d", tv.Type) + } + return +} + +func (sv *stackValue) toTealValue() (tv basics.TealValue) { + if sv.argType() == StackBytes { + return basics.TealValue{Type: basics.TealBytesType, Bytes: string(sv.Bytes)} + } + return basics.TealValue{Type: basics.TealUintType, Uint: sv.Uint} +} + +// LedgerForLogic represents ledger API for Stateful TEAL program +type LedgerForLogic interface { + Balance(addr basics.Address) (basics.MicroAlgos, error) + Round() basics.Round + LatestTimestamp() int64 + AppGlobalState(appIdx basics.AppIndex) (basics.TealKeyValue, error) + AppLocalState(addr basics.Address, appIdx basics.AppIndex) (basics.TealKeyValue, error) + AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) + AssetParams(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetParams, error) + ApplicationID() basics.AppIndex +} + // EvalParams contains data that comes into condition evaluation. type EvalParams struct { // the transaction being evaluated @@ -91,6 +125,47 @@ type EvalParams struct { GroupIndex int Logger logging.Logger + + Ledger LedgerForLogic + + // optional debugger + Debugger DebuggerHook + + // determines eval mode: runModeSignature or runModeApplication + runModeFlags runMode +} + +type opEvalFunc func(cx *evalContext) +type opCheckFunc func(cx *evalContext) int + +type runMode uint64 + +const ( + // runModeSignature is TEAL in LogicSig execution + runModeSignature runMode = 1 << iota + + // runModeApplication is TEAL in application/stateful mode + runModeApplication + + // local constant, run in any mode + modeAny = runModeSignature | runModeApplication +) + +func (r runMode) Any() bool { + return r == modeAny +} + +func (r runMode) String() string { + switch r { + case runModeSignature: + return "Signature" + case runModeApplication: + return "Application" + case modeAny: + return "Any" + default: + } + return "Unknown" } func (ep EvalParams) log() logging.Logger { @@ -100,6 +175,16 @@ func (ep EvalParams) log() logging.Logger { return logging.Base() } +type ckey struct { + app uint64 + addr basics.Address +} + +type indexedCow struct { + accountIdx uint64 + cow *keyValueCow +} + type evalContext struct { EvalParams @@ -120,14 +205,25 @@ type evalContext struct { // If Check pc skips a target, the source branch was invalid! branchTargets []int - programHash crypto.Digest -} + programHashCached crypto.Digest + txidCache map[int]transactions.Txid -type opFunc func(cx *evalContext) + globalStateCow *keyValueCow + readOnlyGlobalStates map[uint64]basics.TealKeyValue + localStateCows map[basics.Address]*indexedCow + readOnlyLocalStates map[ckey]basics.TealKeyValue + appEvalDelta basics.EvalDelta + + // Stores state & disassembly for the optional debugger + debugState DebugState +} // StackType describes the type of a value on the operand stack type StackType byte +// StackTypes is an alias for a list of StackType with syntactic sugar +type StackTypes []StackType + // StackNone in an OpSpec shows that the op pops or yields nothing const StackNone StackType = 0 @@ -154,6 +250,10 @@ func (st StackType) String() string { return "internal error, unknown type" } +func (sts StackTypes) plus(other StackTypes) StackTypes { + return append(sts, other...) +} + // PanicError wraps a recover() catching a panic() type PanicError struct { PanicValue interface{} @@ -165,37 +265,96 @@ func (pe PanicError) Error() string { } var errLoopDetected = errors.New("loop detected") -var errCostTooHigh = errors.New("LogicSigMaxCost exceded") var errLogicSignNotSupported = errors.New("LogicSig not supported") var errTooManyArgs = errors.New("LogicSig has too many arguments") +// EvalStateful executes stateful TEAL program +func EvalStateful(program []byte, params EvalParams) (pass bool, delta basics.EvalDelta, err error) { + var cx evalContext + cx.EvalParams = params + cx.runModeFlags = runModeApplication + + cx.appEvalDelta = basics.EvalDelta{ + GlobalDelta: make(basics.StateDelta), + LocalDeltas: make(map[uint64]basics.StateDelta, len(params.Txn.Txn.Accounts)+1), + } + + // Allocate global delta cow lazily to avoid ledger lookups + cx.globalStateCow = nil + + // Stores read-only global key/value stores keyed off app + cx.readOnlyGlobalStates = make(map[uint64]basics.TealKeyValue) + + // Stores state cows for each modified LocalState for this app + cx.localStateCows = make(map[basics.Address]*indexedCow) + + // Stores read-only local key/value stores keyed off of + cx.readOnlyLocalStates = make(map[ckey]basics.TealKeyValue) + + // Evaluate the program + pass, err = eval(program, &cx) + + // Fill in state deltas + for _, idxCow := range cx.localStateCows { + if len(idxCow.cow.delta) > 0 { + cx.appEvalDelta.LocalDeltas[idxCow.accountIdx] = idxCow.cow.delta + } + } + + return pass, cx.appEvalDelta, err +} + // Eval checks to see if a transaction passes logic -// A program passes succesfully if it finishes with one int element on the stack that is non-zero. +// A program passes successfully if it finishes with one int element on the stack that is non-zero. func Eval(program []byte, params EvalParams) (pass bool, err error) { + var cx evalContext + cx.EvalParams = params + cx.runModeFlags = runModeSignature + return eval(program, &cx) +} + +// eval implementation +// A program passes successfully if it finishes with one int element on the stack that is non-zero. +func eval(program []byte, cx *evalContext) (pass bool, err error) { defer func() { if x := recover(); x != nil { buf := make([]byte, 16*1024) stlen := runtime.Stack(buf, false) pass = false errstr := string(buf[:stlen]) - if params.Trace != nil { - if sb, ok := params.Trace.(*strings.Builder); ok { + if cx.EvalParams.Trace != nil { + if sb, ok := cx.EvalParams.Trace.(*strings.Builder); ok { errstr += sb.String() } } err = PanicError{x, errstr} - params.log().Errorf("recovered panic in Eval: %s", err) + cx.EvalParams.log().Errorf("recovered panic in Eval: %s", err) } }() - if (params.Proto == nil) || (params.Proto.LogicSigVersion == 0) { + + defer func() { + // Ensure we update the debugger before exiting + if cx.Debugger != nil { + errDbg := cx.Debugger.Complete(cx.refreshDebugState()) + if err == nil { + err = errDbg + } + } + }() + + if (cx.EvalParams.Proto == nil) || (cx.EvalParams.Proto.LogicSigVersion == 0) { err = errLogicSignNotSupported return } - if params.Txn.Lsig.Args != nil && len(params.Txn.Lsig.Args) > transactions.EvalMaxArgs { + if cx.EvalParams.Txn.Lsig.Args != nil && len(cx.EvalParams.Txn.Lsig.Args) > transactions.EvalMaxArgs { err = errTooManyArgs return } - var cx evalContext + + if len(program) == 0 { + cx.err = errors.New("invalid program (empty)") + return false, cx.err + } version, vlen := binary.Uvarint(program) if vlen <= 0 { cx.err = errors.New("invalid version") @@ -205,14 +364,18 @@ func Eval(program []byte, params EvalParams) (pass bool, err error) { cx.err = fmt.Errorf("program version %d greater than max supported version %d", version, EvalMaxVersion) return false, cx.err } - if version > params.Proto.LogicSigVersion { - cx.err = fmt.Errorf("program version %d greater than protocol supported version %d", version, params.Proto.LogicSigVersion) + if version > cx.EvalParams.Proto.LogicSigVersion { + cx.err = fmt.Errorf("program version %d greater than protocol supported version %d", version, cx.EvalParams.Proto.LogicSigVersion) return false, cx.err } - if (params.Txn.Txn.RekeyTo != basics.Address{}) { // Currently no TEAL version knows about the RekeyTo field, but this may change in a future TEAL version + + // Currently no TEAL version knows about the RekeyTo field, but this + // may change in a future TEAL version + if !cx.EvalParams.Txn.Txn.RekeyTo.IsZero() { cx.err = fmt.Errorf("program version %d doesn't allow transactions with nonzero RekeyTo field", version) return false, cx.err } + // TODO: if EvalMaxVersion > version, ensure that inaccessible // fields as of the program's version are zero or other // default value so that no one is hiding unexpected @@ -220,24 +383,34 @@ func Eval(program []byte, params EvalParams) (pass bool, err error) { cx.version = version cx.pc = vlen - cx.EvalParams = params cx.stack = make([]stackValue, 0, 10) cx.program = program - cx.programHash = crypto.HashObj(Program(program)) + + if cx.Debugger != nil { + cx.debugState = makeDebugState(cx) + if err = cx.Debugger.Register(cx.refreshDebugState()); err != nil { + return + } + } + for (cx.err == nil) && (cx.pc < len(cx.program)) { + if cx.Debugger != nil { + if err = cx.Debugger.Update(cx.refreshDebugState()); err != nil { + return + } + } + cx.step() cx.stepCount++ if cx.stepCount > len(cx.program) { return false, errLoopDetected } - if uint64(cx.cost) > params.Proto.LogicSigMaxCost { - return false, errCostTooHigh - } } if cx.err != nil { if cx.Trace != nil { fmt.Fprintf(cx.Trace, "%3d %s\n", cx.pc, cx.err) } + return false, cx.err } if len(cx.stack) != 1 { @@ -255,9 +428,21 @@ func Eval(program []byte, params EvalParams) (pass bool, err error) { return cx.stack[0].Uint != 0, nil } +// CheckStateful should be faster than EvalStateful. +// Returns 'cost' which is an estimate of relative execution time. +func CheckStateful(program []byte, params EvalParams) (cost int, err error) { + params.runModeFlags = runModeApplication + return check(program, params) +} + // Check should be faster than Eval. // Returns 'cost' which is an estimate of relative execution time. func Check(program []byte, params EvalParams) (cost int, err error) { + params.runModeFlags = runModeSignature + return check(program, params) +} + +func check(program []byte, params EvalParams) (cost int, err error) { defer func() { if x := recover(); x != nil { buf := make([]byte, 16*1024) @@ -296,7 +481,12 @@ func Check(program []byte, params EvalParams) (cost int, err error) { cx.EvalParams = params cx.program = program for (cx.err == nil) && (cx.pc < len(cx.program)) { + prevpc := cx.pc cost += cx.checkStep() + if cx.pc <= prevpc { + err = fmt.Errorf("pc did not advance, stuck at %d", cx.pc) + return + } } if cx.err != nil { err = fmt.Errorf("%3d %s", cx.pc, cx.err) @@ -305,138 +495,6 @@ func Check(program []byte, params EvalParams) (cost int, err error) { return } -// OpSpec defines one byte opcode -type OpSpec struct { - Opcode byte - Name string - op opFunc // evaluate the op - Args []StackType // what gets popped from the stack - Returns []StackType // what gets pushed to the stack -} - -var oneBytes = []StackType{StackBytes} -var threeBytes = []StackType{StackBytes, StackBytes, StackBytes} -var oneInt = []StackType{StackUint64} -var twoInts = []StackType{StackUint64, StackUint64} -var oneAny = []StackType{StackAny} -var twoAny = []StackType{StackAny, StackAny} - -// OpSpecs is the table of operations that can be assembled and evaluated. -// -// Any changes should be reflected in README.md which serves as the language spec. -var OpSpecs = []OpSpec{ - {0x00, "err", opErr, nil, nil}, - {0x01, "sha256", opSHA256, oneBytes, oneBytes}, - {0x02, "keccak256", opKeccak256, oneBytes, oneBytes}, - {0x03, "sha512_256", opSHA512_256, oneBytes, oneBytes}, - {0x04, "ed25519verify", opEd25519verify, threeBytes, oneInt}, - {0x08, "+", opPlus, twoInts, oneInt}, - {0x09, "-", opMinus, twoInts, oneInt}, - {0x0a, "/", opDiv, twoInts, oneInt}, - {0x0b, "*", opMul, twoInts, oneInt}, - {0x0c, "<", opLt, twoInts, oneInt}, - {0x0d, ">", opGt, twoInts, oneInt}, - {0x0e, "<=", opLe, twoInts, oneInt}, - {0x0f, ">=", opGe, twoInts, oneInt}, - {0x10, "&&", opAnd, twoInts, oneInt}, - {0x11, "||", opOr, twoInts, oneInt}, - {0x12, "==", opEq, twoAny, oneInt}, - {0x13, "!=", opNeq, twoAny, oneInt}, - {0x14, "!", opNot, oneInt, oneInt}, - {0x15, "len", opLen, oneBytes, oneInt}, - {0x16, "itob", opItob, oneInt, oneBytes}, - {0x17, "btoi", opBtoi, oneBytes, oneInt}, - {0x18, "%", opModulo, twoInts, oneInt}, - {0x19, "|", opBitOr, twoInts, oneInt}, - {0x1a, "&", opBitAnd, twoInts, oneInt}, - {0x1b, "^", opBitXor, twoInts, oneInt}, - {0x1c, "~", opBitNot, oneInt, oneInt}, - {0x1d, "mulw", opMulw, twoInts, twoInts}, - - {0x20, "intcblock", opIntConstBlock, nil, nil}, - {0x21, "intc", opIntConstLoad, nil, oneInt}, - {0x22, "intc_0", opIntConst0, nil, oneInt}, - {0x23, "intc_1", opIntConst1, nil, oneInt}, - {0x24, "intc_2", opIntConst2, nil, oneInt}, - {0x25, "intc_3", opIntConst3, nil, oneInt}, - {0x26, "bytecblock", opByteConstBlock, nil, nil}, - {0x27, "bytec", opByteConstLoad, nil, oneBytes}, - {0x28, "bytec_0", opByteConst0, nil, oneBytes}, - {0x29, "bytec_1", opByteConst1, nil, oneBytes}, - {0x2a, "bytec_2", opByteConst2, nil, oneBytes}, - {0x2b, "bytec_3", opByteConst3, nil, oneBytes}, - {0x2c, "arg", opArg, nil, oneBytes}, - {0x2d, "arg_0", opArg0, nil, oneBytes}, - {0x2e, "arg_1", opArg1, nil, oneBytes}, - {0x2f, "arg_2", opArg2, nil, oneBytes}, - {0x30, "arg_3", opArg3, nil, oneBytes}, - {0x31, "txn", opTxn, nil, oneAny}, // TODO: check output type by subfield retrieved in txn,global,account,txid - {0x32, "global", opGlobal, nil, oneAny}, // TODO: check output type against specific field - {0x33, "gtxn", opGtxn, nil, oneAny}, // TODO: check output type by subfield retrieved in txn,global,account,txid - {0x34, "load", opLoad, nil, oneAny}, - {0x35, "store", opStore, oneAny, nil}, - - {0x40, "bnz", opBnz, oneInt, nil}, - {0x48, "pop", opPop, oneAny, nil}, - {0x49, "dup", opDup, oneAny, twoAny}, -} - -// direct opcode bytes -var opsByOpcode []OpSpec - -type opCheckFunc func(cx *evalContext) int - -// opSize records the length in bytes for an op that is constant-length but not length 1 -type opSize struct { - name string - cost int - size int - checkFunc opCheckFunc -} - -// opSizes records the size of ops that are constant size but not 1 -// Also records time 'cost' and custom check functions. -var opSizes = []opSize{ - {"sha256", 7, 1, nil}, - {"keccak256", 26, 1, nil}, - {"sha512_256", 9, 1, nil}, - {"ed25519verify", 1900, 1, nil}, - {"bnz", 1, 3, checkBnz}, - {"intc", 1, 2, nil}, - {"bytec", 1, 2, nil}, - {"arg", 1, 2, nil}, - {"txn", 1, 2, nil}, - {"gtxn", 1, 3, nil}, - {"global", 1, 2, nil}, - {"intcblock", 1, 0, checkIntConstBlock}, - {"bytecblock", 1, 0, checkByteConstBlock}, - {"load", 1, 2, nil}, - {"store", 1, 2, nil}, -} - -var opSizeByOpcode []opSize - -func init() { - opsByOpcode = make([]OpSpec, 256) - for _, oi := range OpSpecs { - opsByOpcode[oi.Opcode] = oi - } - - opSizeByName := make(map[string]*opSize, len(opSizes)) - for i, oz := range opSizes { - opSizeByName[oz.name] = &opSizes[i] - } - opSizeByOpcode = make([]opSize, 256) - for _, oi := range OpSpecs { - oz := opSizeByName[oi.Name] - if oz == nil { - opSizeByOpcode[oi.Opcode] = opSize{oi.Name, 1, 1, nil} - } else { - opSizeByOpcode[oi.Opcode] = *oz - } - } -} - func opCompat(expected, got StackType) bool { if expected == StackAny { return true @@ -456,42 +514,68 @@ const MaxStackDepth = 1000 func (cx *evalContext) step() { opcode := cx.program[cx.pc] - if opsByOpcode[opcode].op == nil { - cx.err = fmt.Errorf("%3d illegal opcode %02x", cx.pc, opcode) + spec := &opsByOpcode[cx.version][opcode] + + // this check also ensures TEAL versioning: v2 opcodes are not in opsByOpcode[1] array + if spec.op == nil { + cx.err = fmt.Errorf("%3d illegal opcode 0x%02x", cx.pc, opcode) + return + } + if (cx.runModeFlags & spec.Modes) == 0 { + cx.err = fmt.Errorf("%s not allowed in current mode", spec.Name) + return + } + argsTypes := spec.Args + + // check args for stack underflow and types + if len(cx.stack) < len(argsTypes) { + cx.err = fmt.Errorf("stack underflow in %s", spec.Name) return } - argsTypes := opsByOpcode[opcode].Args - if len(argsTypes) >= 0 { - // check args for stack underflow and types - if len(cx.stack) < len(argsTypes) { - cx.err = fmt.Errorf("stack underflow in %s", opsByOpcode[opcode].Name) + first := len(cx.stack) - len(argsTypes) + for i, argType := range argsTypes { + if !opCompat(argType, cx.stack[first+i].argType()) { + cx.err = fmt.Errorf("%s arg %d wanted %s but got %s", spec.Name, i, argType.String(), cx.stack[first+i].typeName()) return } - first := len(cx.stack) - len(argsTypes) - for i, argType := range argsTypes { - if !opCompat(argType, cx.stack[first+i].argType()) { - cx.err = fmt.Errorf("%s arg %d wanted %s but got %s", opsByOpcode[opcode].Name, i, argType.String(), cx.stack[first+i].typeName()) - return - } - } } - oz := opSizeByOpcode[opcode] + + oz := spec.opSize if oz.size != 0 && (cx.pc+oz.size > len(cx.program)) { - cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, opsByOpcode[opcode].Name) + cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, spec.Name) return } cx.cost += oz.cost - opsByOpcode[opcode].op(cx) + spec.op(cx) if cx.Trace != nil { + immArgsString := " " + if spec.Name != "bnz" { + for i := 1; i < spec.opSize.size; i++ { + immArgsString += fmt.Sprintf("0x%02x ", cx.program[cx.pc+i]) + } + } + var stackString string if len(cx.stack) == 0 { - fmt.Fprintf(cx.Trace, "%3d %s => %s\n", cx.pc, opsByOpcode[opcode].Name, "") + stackString = "" } else { - fmt.Fprintf(cx.Trace, "%3d %s => %s\n", cx.pc, opsByOpcode[opcode].Name, cx.stack[len(cx.stack)-1].String()) + num := 1 + if len(spec.Returns) > 1 { + num = len(spec.Returns) + } + if len(cx.stack) < num { + cx.err = fmt.Errorf("stack underflow: expected %d, have %d", num, len(cx.stack)) + return + } + for i := 1; i <= num; i++ { + stackString += fmt.Sprintf("(%s) ", cx.stack[len(cx.stack)-i].String()) + } } + fmt.Fprintf(cx.Trace, "%3d %s%s=> %s\n", cx.pc, spec.Name, immArgsString, stackString) } if cx.err != nil { return } + if len(cx.stack) > MaxStackDepth { cx.err = errors.New("stack overflow") return @@ -506,15 +590,21 @@ func (cx *evalContext) step() { func (cx *evalContext) checkStep() (cost int) { opcode := cx.program[cx.pc] - if opsByOpcode[opcode].op == nil { - cx.err = fmt.Errorf("%3d illegal opcode %02x", cx.pc, opcode) + spec := &opsByOpcode[cx.version][opcode] + if spec.op == nil { + cx.err = fmt.Errorf("%3d illegal opcode 0x%02x", cx.pc, opcode) return 1 } - oz := opSizeByOpcode[opcode] + if (cx.runModeFlags & spec.Modes) == 0 { + cx.err = fmt.Errorf("%s not allowed in current mode", spec.Name) + return + } + oz := spec.opSize if oz.size != 0 && (cx.pc+oz.size > len(cx.program)) { - cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, opsByOpcode[opcode].Name) + cx.err = fmt.Errorf("%3d %s program ends short of immediate values", cx.pc, spec.Name) return 1 } + prevpc := cx.pc if oz.checkFunc != nil { cost = oz.checkFunc(cx) if cx.nextpc != 0 { @@ -527,6 +617,9 @@ func (cx *evalContext) checkStep() (cost int) { cost = oz.cost cx.pc += oz.size } + if cx.Trace != nil { + fmt.Fprintf(cx.Trace, "%3d %s\n", prevpc, spec.Name) + } if cx.err != nil { return 1 } @@ -544,7 +637,17 @@ func (cx *evalContext) checkStep() (cost int) { } func opErr(cx *evalContext) { - cx.err = errors.New("error") + cx.err = errors.New("TEAL runtime encountered err opcode") +} + +func opReturn(cx *evalContext) { + // Achieve the end condition: + // Take the last element on the stack and make it the return value (only element on the stack) + // Move the pc to the end of the program + last := len(cx.stack) - 1 + cx.stack[0] = cx.stack[last] + cx.stack = cx.stack[:1] + cx.nextpc = len(cx.program) } func opSHA256(cx *evalContext) { @@ -805,6 +908,8 @@ func opItob(cx *evalContext) { last := len(cx.stack) - 1 ibytes := make([]byte, 8) binary.BigEndian.PutUint64(ibytes, cx.stack[last].Uint) + // cx.stack[last].Uint is not cleared out as optimization + // stackValue.argType() checks Bytes field first cx.stack[last].Bytes = ibytes } @@ -916,6 +1021,7 @@ func opArgN(cx *evalContext, n uint64) { val := nilToEmpty(cx.Txn.Lsig.Args[n]) cx.stack = append(cx.stack, stackValue{Bytes: val}) } + func opArg(cx *evalContext) { n := uint64(cx.program[cx.pc+1]) opArgN(cx, n) @@ -934,16 +1040,24 @@ func opArg3(cx *evalContext) { opArgN(cx, 3) } -func checkBnz(cx *evalContext) int { +// checks any branch that is {op} {int16 be offset} +func checkBranch(cx *evalContext) int { offset := (uint(cx.program[cx.pc+1]) << 8) | uint(cx.program[cx.pc+2]) if offset > 0x7fff { - cx.err = fmt.Errorf("bnz offset %x too large", offset) + cx.err = fmt.Errorf("branch offset %x too large", offset) return 1 } cx.nextpc = cx.pc + 3 target := cx.nextpc + int(offset) - if target >= len(cx.program) { - cx.err = errors.New("bnz target beyond end of program") + var branchTooFar bool + if cx.version >= 2 { + // branching to exactly the end of the program (target == len(cx.program)), the next pc after the last instruction, is okay and ends normally + branchTooFar = target > len(cx.program) + } else { + branchTooFar = target >= len(cx.program) + } + if branchTooFar { + cx.err = errors.New("branch target beyond end of program") return 1 } cx.branchTargets = append(cx.branchTargets, target) @@ -965,6 +1079,30 @@ func opBnz(cx *evalContext) { } } +func opBz(cx *evalContext) { + last := len(cx.stack) - 1 + cx.nextpc = cx.pc + 3 + isZero := cx.stack[last].Uint == 0 + cx.stack = cx.stack[:last] // pop + if isZero { + offset := (uint(cx.program[cx.pc+1]) << 8) | uint(cx.program[cx.pc+2]) + if offset > 0x7fff { + cx.err = fmt.Errorf("bz offset %x too large", offset) + return + } + cx.nextpc += int(offset) + } +} + +func opB(cx *evalContext) { + offset := (uint(cx.program[cx.pc+1]) << 8) | uint(cx.program[cx.pc+2]) + if offset > 0x7fff { + cx.err = fmt.Errorf("b offset %x too large", offset) + return + } + cx.nextpc = cx.pc + 3 + int(offset) +} + func opPop(cx *evalContext) { last := len(cx.stack) - 1 cx.stack = cx.stack[:last] @@ -976,9 +1114,102 @@ func opDup(cx *evalContext) { cx.stack = append(cx.stack, sv) } -func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field uint64) (sv stackValue, err error) { +func opDup2(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + cx.stack = append(cx.stack, cx.stack[prev:]...) +} + +func (cx *evalContext) assetHoldingEnumToValue(holding *basics.AssetHolding, field uint64) (sv stackValue, err error) { + switch AssetHoldingField(field) { + case AssetBalance: + sv.Uint = holding.Amount + case AssetFrozen: + if holding.Frozen { + sv.Uint = 1 + } else { + sv.Uint = 0 + } + default: + err = fmt.Errorf("invalid asset holding field %d", field) + return + } + + assetHoldingField := AssetHoldingField(field) + assetHoldingFieldType := AssetHoldingFieldTypes[assetHoldingField] + if assetHoldingFieldType != sv.argType() { + err = fmt.Errorf("%s expected field type is %s but got %s", assetHoldingField.String(), assetHoldingFieldType.String(), sv.argType().String()) + } + return +} + +func (cx *evalContext) assetParamsEnumToValue(params *basics.AssetParams, field uint64) (sv stackValue, err error) { + switch AssetParamsField(field) { + case AssetTotal: + sv.Uint = params.Total + case AssetDecimals: + sv.Uint = uint64(params.Decimals) + case AssetDefaultFrozen: + if params.DefaultFrozen { + sv.Uint = 1 + } else { + sv.Uint = 0 + } + case AssetUnitName: + sv.Bytes = []byte(params.UnitName) + case AssetAssetName: + sv.Bytes = []byte(params.AssetName) + case AssetURL: + sv.Bytes = []byte(params.URL) + case AssetMetadataHash: + sv.Bytes = params.MetadataHash[:] + case AssetManager: + sv.Bytes = params.Manager[:] + case AssetReserve: + sv.Bytes = params.Reserve[:] + case AssetFreeze: + sv.Bytes = params.Freeze[:] + case AssetClawback: + sv.Bytes = params.Clawback[:] + default: + err = fmt.Errorf("invalid asset params field %d", field) + return + } + + assetParamsField := AssetParamsField(field) + assetParamsFieldType := AssetParamsFieldTypes[assetParamsField] + if assetParamsFieldType != sv.argType() { + err = fmt.Errorf("%s expected field type is %s but got %s", assetParamsField.String(), assetParamsFieldType.String(), sv.argType().String()) + } + return +} + +// TxnFieldToTealValue is a thin wrapper for txnFieldToStack for external use +func TxnFieldToTealValue(txn *transactions.Transaction, groupIndex int, field TxnField) (basics.TealValue, error) { + cx := evalContext{EvalParams: EvalParams{GroupIndex: groupIndex}} + sv, err := cx.txnFieldToStack(txn, field, 0, groupIndex) + return sv.toTealValue(), err +} + +func (cx *evalContext) getTxID(txn *transactions.Transaction, groupIndex int) transactions.Txid { + // Initialize txidCache if necessary + if cx.txidCache == nil { + cx.txidCache = make(map[int]transactions.Txid, len(cx.TxnGroup)) + } + + // Hashes are expensive, so we cache computed TxIDs + txid, ok := cx.txidCache[groupIndex] + if !ok { + txid = txn.ID() + cx.txidCache[groupIndex] = txid + } + + return txid +} + +func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnField, arrayFieldIdx uint64, groupIndex int) (sv stackValue, err error) { err = nil - switch TxnField(field) { + switch field { case Sender: sv.Bytes = txn.Sender[:] case Fee: @@ -1020,23 +1251,67 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field uint case AssetCloseTo: sv.Bytes = txn.AssetCloseTo[:] case GroupIndex: - sv.Uint = uint64(cx.GroupIndex) + sv.Uint = uint64(groupIndex) case TxID: - txid := txn.ID() + txid := cx.getTxID(txn, groupIndex) sv.Bytes = txid[:] case Lease: sv.Bytes = txn.Lease[:] + case ApplicationID: + sv.Uint = uint64(txn.ApplicationID) + case OnCompletion: + sv.Uint = uint64(txn.OnCompletion) + case ApplicationArgs: + if arrayFieldIdx >= uint64(len(txn.ApplicationArgs)) { + err = fmt.Errorf("invalid ApplicationArgs index %d", arrayFieldIdx) + return + } + sv.Bytes = make([]byte, len(txn.ApplicationArgs[arrayFieldIdx])) + copy(sv.Bytes, txn.ApplicationArgs[arrayFieldIdx]) + case NumAppArgs: + sv.Uint = uint64(len(txn.ApplicationArgs)) + case Accounts: + if arrayFieldIdx == 0 { + // special case: sender + sv.Bytes = txn.Sender[:] + } else { + if arrayFieldIdx > uint64(len(txn.Accounts)) { + err = fmt.Errorf("invalid Accounts index %d", arrayFieldIdx) + return + } + sv.Bytes = txn.Accounts[arrayFieldIdx-1][:] + } + case NumAccounts: + sv.Uint = uint64(len(txn.Accounts)) + case ApprovalProgram: + sv.Bytes = make([]byte, len(txn.ApprovalProgram)) + copy(sv.Bytes, txn.ApprovalProgram) + case ClearStateProgram: + sv.Bytes = make([]byte, len(txn.ClearStateProgram)) + copy(sv.Bytes, txn.ClearStateProgram) default: err = fmt.Errorf("invalid txn field %d", field) + return + } + + txnField := TxnField(field) + txnFieldType := TxnFieldTypes[txnField] + if txnFieldType != sv.argType() { + err = fmt.Errorf("%s expected field type is %s but got %s", txnField.String(), txnFieldType.String(), sv.argType().String()) } return } func opTxn(cx *evalContext) { - field := uint64(cx.program[cx.pc+1]) + field := TxnField(uint64(cx.program[cx.pc+1])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version || field == ApplicationArgs || field == Accounts { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } var sv stackValue var err error - sv, err = cx.txnFieldToStack(&cx.Txn.Txn, field) + sv, err = cx.txnFieldToStack(&cx.Txn.Txn, field, 0, cx.GroupIndex) if err != nil { cx.err = err return @@ -1045,6 +1320,29 @@ func opTxn(cx *evalContext) { cx.nextpc = cx.pc + 2 } +func opTxna(cx *evalContext) { + field := TxnField(uint64(cx.program[cx.pc+1])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + if field != ApplicationArgs && field != Accounts { + cx.err = fmt.Errorf("txna unsupported field %d", field) + return + } + var sv stackValue + var err error + arrayFieldIdx := uint64(cx.program[cx.pc+2]) + sv, err = cx.txnFieldToStack(&cx.Txn.Txn, field, arrayFieldIdx, cx.GroupIndex) + if err != nil { + cx.err = err + return + } + cx.stack = append(cx.stack, sv) + cx.nextpc = cx.pc + 3 +} + func opGtxn(cx *evalContext) { gtxid := int(uint(cx.program[cx.pc+1])) if gtxid >= len(cx.TxnGroup) { @@ -1052,14 +1350,19 @@ func opGtxn(cx *evalContext) { return } tx := &cx.TxnGroup[gtxid].Txn - field := uint64(cx.program[cx.pc+2]) + field := TxnField(uint64(cx.program[cx.pc+2])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version || field == ApplicationArgs || field == Accounts { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } var sv stackValue var err error if TxnField(field) == GroupIndex { // GroupIndex; asking this when we just specified it is _dumb_, but oh well sv.Uint = uint64(gtxid) } else { - sv, err = cx.txnFieldToStack(tx, field) + sv, err = cx.txnFieldToStack(tx, field, 0, gtxid) if err != nil { cx.err = err return @@ -1069,12 +1372,60 @@ func opGtxn(cx *evalContext) { cx.nextpc = cx.pc + 3 } +func opGtxna(cx *evalContext) { + gtxid := int(uint(cx.program[cx.pc+1])) + if gtxid >= len(cx.TxnGroup) { + cx.err = fmt.Errorf("gtxna lookup TxnGroup[%d] but it only has %d", gtxid, len(cx.TxnGroup)) + return + } + tx := &cx.TxnGroup[gtxid].Txn + field := TxnField(uint64(cx.program[cx.pc+2])) + fs, ok := txnFieldSpecByField[field] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + if TxnField(field) != ApplicationArgs && TxnField(field) != Accounts { + cx.err = fmt.Errorf("gtxna unsupported field %d", field) + return + } + var sv stackValue + var err error + arrayFieldIdx := uint64(cx.program[cx.pc+3]) + sv, err = cx.txnFieldToStack(tx, field, arrayFieldIdx, gtxid) + if err != nil { + cx.err = err + return + } + cx.stack = append(cx.stack, sv) + cx.nextpc = cx.pc + 4 +} + +func (cx *evalContext) getRound() (rnd uint64, err error) { + if cx.Ledger == nil { + err = fmt.Errorf("ledger not available") + return + } + return uint64(cx.Ledger.Round()), nil +} + +func (cx *evalContext) getLatestTimestamp() (timestamp uint64, err error) { + if cx.Ledger == nil { + err = fmt.Errorf("ledger not available") + return + } + ts := cx.Ledger.LatestTimestamp() + if ts < 0 { + err = fmt.Errorf("latest timestamp %d < 0", ts) + return + } + return uint64(ts), nil +} + var zeroAddress basics.Address -func opGlobal(cx *evalContext) { - gindex := uint64(cx.program[cx.pc+1]) - var sv stackValue - switch GlobalField(gindex) { +func (cx *evalContext) globalFieldToStack(field GlobalField) (sv stackValue, err error) { + switch field { case MinTxnFee: sv.Uint = cx.Proto.MinTxnFee case MinBalance: @@ -1085,10 +1436,43 @@ func opGlobal(cx *evalContext) { sv.Bytes = zeroAddress[:] case GroupSize: sv.Uint = uint64(len(cx.TxnGroup)) + case LogicSigVersion: + sv.Uint = cx.Proto.LogicSigVersion + case Round: + sv.Uint, err = cx.getRound() + case LatestTimestamp: + sv.Uint, err = cx.getLatestTimestamp() default: - cx.err = fmt.Errorf("invalid global[%d]", gindex) + err = fmt.Errorf("invalid global[%d]", field) + } + return sv, err +} + +func opGlobal(cx *evalContext) { + gindex := uint64(cx.program[cx.pc+1]) + globalField := GlobalField(gindex) + fs, ok := globalFieldSpecByField[globalField] + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid global[%d]", globalField) + return + } + if (cx.runModeFlags & fs.mode) == 0 { + cx.err = fmt.Errorf("global[%d] not allowed in current mode", globalField) + return + } + + sv, err := cx.globalFieldToStack(globalField) + if err != nil { + cx.err = err + return + } + + globalFieldType := GlobalFieldTypes[globalField] + if globalFieldType != sv.argType() { + cx.err = fmt.Errorf("%s expected field type is %s but got %s", globalField.String(), globalFieldType.String(), sv.argType().String()) return } + cx.stack = append(cx.stack, sv) cx.nextpc = cx.pc + 2 } @@ -1106,8 +1490,16 @@ func (msg Msg) ToBeHashed() (protocol.HashID, []byte) { return protocol.ProgramData, append(msg.ProgramHash[:], msg.Data...) } -func opEd25519verify(cx *evalContext) { - last := len(cx.stack) - 1 // index of PK +// programHash lets us lazily compute H(cx.program) +func (cx *evalContext) programHash() crypto.Digest { + if cx.programHashCached == (crypto.Digest{}) { + cx.programHashCached = crypto.HashObj(Program(cx.program)) + } + return cx.programHashCached +} + +func opEd25519verify(cx *evalContext) { + last := len(cx.stack) - 1 // index of PK prev := last - 1 // index of signature pprev := prev - 1 // index of data @@ -1125,7 +1517,7 @@ func opEd25519verify(cx *evalContext) { } copy(sig[:], cx.stack[prev].Bytes) - msg := Msg{ProgramHash: cx.programHash, Data: cx.stack[pprev].Bytes} + msg := Msg{ProgramHash: cx.programHash(), Data: cx.stack[pprev].Bytes} if sv.Verify(msg, sig) { cx.stack[pprev].Uint = 1 } else { @@ -1148,3 +1540,539 @@ func opStore(cx *evalContext) { cx.stack = cx.stack[:last] cx.nextpc = cx.pc + 2 } + +func opConcat(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + a := cx.stack[prev].Bytes + b := cx.stack[last].Bytes + newlen := len(a) + len(b) + if newlen > MaxStringSize { + cx.err = errors.New("concat resulted in string too long") + return + } + newvalue := make([]byte, newlen) + copy(newvalue, a) + copy(newvalue[len(a):], b) + cx.stack[prev].Bytes = newvalue + cx.stack = cx.stack[:last] +} + +func substring(x []byte, start, end int) (out []byte, err error) { + out = x + if end <= start { + err = errors.New("substring end before start") + return + } + if start > len(x) || end > len(x) { + err = errors.New("substring range beyond length of string") + return + } + out = x[start:end] + err = nil + return +} + +func opSubstring(cx *evalContext) { + last := len(cx.stack) - 1 + start := cx.program[cx.pc+1] + end := cx.program[cx.pc+2] + cx.stack[last].Bytes, cx.err = substring(cx.stack[last].Bytes, int(start), int(end)) + cx.nextpc = cx.pc + 3 +} + +func opSubstring3(cx *evalContext) { + last := len(cx.stack) - 1 // end + prev := last - 1 // start + pprev := prev - 1 // bytes + start := cx.stack[prev].Uint + end := cx.stack[last].Uint + if start > math.MaxInt32 || end > math.MaxInt32 { + cx.err = errors.New("substring range beyond length of string") + return + } + cx.stack[pprev].Bytes, cx.err = substring(cx.stack[pprev].Bytes, int(start), int(end)) + cx.stack = cx.stack[:prev] +} + +func opBalance(cx *evalContext) { + last := len(cx.stack) - 1 // account offset + + accountIdx := cx.stack[last].Uint + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + cx.err = err + return + } + + microAlgos, err := cx.Ledger.Balance(addr) + if err != nil { + cx.err = fmt.Errorf("failed to fetch balance of %v: %s", addr, err.Error()) + return + } + + cx.stack[last].Uint = microAlgos.Raw +} + +func opAppCheckOptedIn(cx *evalContext) { + last := len(cx.stack) - 1 // app id + prev := last - 1 // account offset + + appID := cx.stack[last].Uint + accountIdx := cx.stack[prev].Uint + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + cx.err = err + return + } + + _, err = cx.Ledger.AppLocalState(addr, basics.AppIndex(appID)) + if err != nil { + cx.stack[prev].Uint = 0 + } else { + cx.stack[prev].Uint = 1 + } + + cx.stack = cx.stack[:last] +} + +func (cx *evalContext) getReadOnlyLocalState(appID uint64, accountIdx uint64) (basics.TealKeyValue, error) { + // Convert the account offset to an address + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + return nil, err + } + + kvIdx := ckey{appID, addr} + localKV, ok := cx.readOnlyLocalStates[kvIdx] + if !ok { + var err error + localKV, err = cx.Ledger.AppLocalState(addr, basics.AppIndex(appID)) + if err != nil { + return nil, fmt.Errorf("failed to fetch app local state for acct %v, app %d: %v", addr, appID, err) + } + cx.readOnlyLocalStates[kvIdx] = localKV + } + return localKV, nil +} + +func (cx *evalContext) getLocalStateCow(accountIdx uint64) (*keyValueCow, error) { + // Convert the account offset to an address + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + return nil, err + } + + // We key localStateCows by address and not accountIdx, because + // multiple accountIdxs may refer to the same address + idxCow, ok := cx.localStateCows[addr] + if !ok { + // No cached cow for this address. Make one. + localKV, err := cx.Ledger.AppLocalState(addr, basics.AppIndex(cx.Txn.Txn.ApplicationID)) + if err != nil { + return nil, fmt.Errorf("failed to fetch app local state for acct %v: %v", addr, err) + } + + localDelta := make(basics.StateDelta) + kvCow := makeKeyValueCow(localKV, localDelta) + idxCow = &indexedCow{accountIdx, kvCow} + cx.localStateCows[addr] = idxCow + } + return idxCow.cow, nil +} + +func (cx *evalContext) appReadLocalKey(appID uint64, accountIdx uint64, key string) (basics.TealValue, bool, error) { + // If this is for the application mentioned in the transaction header, + // return the result from a LocalState cow, since we may have written + // to it + if appID == 0 || appID == uint64(cx.Ledger.ApplicationID()) { + kvCow, err := cx.getLocalStateCow(accountIdx) + if err != nil { + return basics.TealValue{}, false, err + } + tv, ok := kvCow.read(key) + return tv, ok, nil + } + + // Otherwise, the state is read only, so return from the read only cache + kv, err := cx.getReadOnlyLocalState(appID, accountIdx) + if err != nil { + return basics.TealValue{}, false, err + } + tv, ok := kv[key] + return tv, ok, nil +} + +// appWriteLocalKey writes value to local key/value cow +func (cx *evalContext) appWriteLocalKey(accountIdx uint64, key string, tv basics.TealValue) error { + kvCow, err := cx.getLocalStateCow(accountIdx) + if err != nil { + return err + } + kvCow.write(key, tv) + return nil +} + +// appDeleteLocalKey deletes a value from the key/value cow +func (cx *evalContext) appDeleteLocalKey(accountIdx uint64, key string) error { + kvCow, err := cx.getLocalStateCow(accountIdx) + if err != nil { + return err + } + kvCow.del(key) + return nil +} + +func (cx *evalContext) getReadOnlyGlobalState(appID uint64) (basics.TealKeyValue, error) { + globalKV, ok := cx.readOnlyGlobalStates[appID] + if !ok { + var err error + globalKV, err = cx.Ledger.AppGlobalState(basics.AppIndex(appID)) + if err != nil { + return nil, fmt.Errorf("failed to fetch global state for app %d: %v", appID, err) + } + cx.readOnlyGlobalStates[appID] = globalKV + } + return globalKV, nil +} + +func (cx *evalContext) getGlobalStateCow() (*keyValueCow, error) { + if cx.globalStateCow == nil { + globalKV, err := cx.Ledger.AppGlobalState(cx.Txn.Txn.ApplicationID) + if err != nil { + return nil, fmt.Errorf("failed to fetch global state: %v", err) + } + cx.globalStateCow = makeKeyValueCow(globalKV, cx.appEvalDelta.GlobalDelta) + } + return cx.globalStateCow, nil +} + +func (cx *evalContext) appReadGlobalKey(appID uint64, key string) (basics.TealValue, bool, error) { + // If this is for the application mentioned in the transaction header, + // return the result from a GlobalState cow, since we may have written + // to it + if appID == 0 || appID == uint64(cx.Ledger.ApplicationID()) { + kvCow, err := cx.getGlobalStateCow() + if err != nil { + return basics.TealValue{}, false, err + } + tv, ok := kvCow.read(key) + return tv, ok, nil + } + + // Otherwise, the state is read only, so return from the read only cache + kv, err := cx.getReadOnlyGlobalState(appID) + if err != nil { + return basics.TealValue{}, false, err + } + tv, ok := kv[key] + return tv, ok, nil +} + +// appWriteGlobalKey adds value to StateDelta +func (cx *evalContext) appWriteGlobalKey(key string, tv basics.TealValue) error { + kvCow, err := cx.getGlobalStateCow() + if err != nil { + return err + } + kvCow.write(key, tv) + return nil +} + +// appDeleteGlobalKey deletes a value from the cache and adds it to StateDelta +func (cx *evalContext) appDeleteGlobalKey(key string) error { + kvCow, err := cx.getGlobalStateCow() + if err != nil { + return err + } + kvCow.del(key) + return nil +} + +func opAppGetLocalState(cx *evalContext) { + last := len(cx.stack) - 1 // state key + prev := last - 1 // account offset + + key := cx.stack[last].Bytes + accountIdx := cx.stack[prev].Uint + + var appID uint64 = 0 + result, _, err := opAppGetLocalStateImpl(cx, appID, key, accountIdx) + if err != nil { + cx.err = err + return + } + + cx.stack[prev] = result + cx.stack = cx.stack[:last] +} + +func opAppGetLocalStateEx(cx *evalContext) { + last := len(cx.stack) - 1 // state key + prev := last - 1 // app id + pprev := prev - 1 // account offset + + key := cx.stack[last].Bytes + appID := cx.stack[prev].Uint + accountIdx := cx.stack[pprev].Uint + + result, ok, err := opAppGetLocalStateImpl(cx, appID, key, accountIdx) + if err != nil { + cx.err = err + return + } + + var isOk stackValue + if ok { + isOk.Uint = 1 + } + + cx.stack[pprev] = result + cx.stack[prev] = isOk + cx.stack = cx.stack[:last] +} + +func opAppGetLocalStateImpl(cx *evalContext, appID uint64, key []byte, accountIdx uint64) (result stackValue, ok bool, err error) { + if cx.Ledger == nil { + err = fmt.Errorf("ledger not available") + return + } + + tv, ok, err := cx.appReadLocalKey(appID, accountIdx, string(key)) + if err != nil { + cx.err = err + return + } + + if ok { + result, err = stackValueFromTealValue(&tv) + } + return +} + +func opAppGetGlobalStateImpl(cx *evalContext, appID uint64, key []byte) (result stackValue, ok bool, err error) { + if cx.Ledger == nil { + err = fmt.Errorf("ledger not available") + return + } + + tv, ok, err := cx.appReadGlobalKey(appID, string(key)) + if err != nil { + return + } + + if ok { + result, err = stackValueFromTealValue(&tv) + } + return +} + +func opAppGetGlobalState(cx *evalContext) { + last := len(cx.stack) - 1 // state key + + key := cx.stack[last].Bytes + + var appID uint64 = 0 + result, _, err := opAppGetGlobalStateImpl(cx, appID, key) + if err != nil { + cx.err = err + return + } + + cx.stack[last] = result +} + +func opAppGetGlobalStateEx(cx *evalContext) { + last := len(cx.stack) - 1 // state key + prev := last - 1 + + key := cx.stack[last].Bytes + appID := cx.stack[prev].Uint + + if appID != 0 && appID == uint64(cx.Txn.Txn.ApplicationID) { + appID = 0 // 0 is an alias for the current app + } + + result, ok, err := opAppGetGlobalStateImpl(cx, appID, key) + if err != nil { + cx.err = err + return + } + + var isOk stackValue + if ok { + isOk.Uint = 1 + } + + cx.stack[prev] = result + cx.stack[last] = isOk +} + +func opAppPutLocalState(cx *evalContext) { + last := len(cx.stack) - 1 // value + prev := last - 1 // state key + pprev := prev - 1 // account offset + + sv := cx.stack[last] + key := string(cx.stack[prev].Bytes) + accountIdx := cx.stack[pprev].Uint + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + err := cx.appWriteLocalKey(accountIdx, key, sv.toTealValue()) + if err != nil { + cx.err = err + return + } + + cx.stack = cx.stack[:pprev] +} + +func opAppPutGlobalState(cx *evalContext) { + last := len(cx.stack) - 1 // value + prev := last - 1 // state key + + sv := cx.stack[last] + key := string(cx.stack[prev].Bytes) + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + err := cx.appWriteGlobalKey(key, sv.toTealValue()) + if err != nil { + cx.err = err + return + } + + cx.stack = cx.stack[:prev] +} + +func opAppDeleteLocalState(cx *evalContext) { + last := len(cx.stack) - 1 // key + prev := last - 1 // account offset + + key := string(cx.stack[last].Bytes) + accountIdx := cx.stack[prev].Uint + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + err := cx.appDeleteLocalKey(accountIdx, key) + if err != nil { + cx.err = err + return + } + + cx.stack = cx.stack[:prev] +} + +func opAppDeleteGlobalState(cx *evalContext) { + last := len(cx.stack) - 1 // key + + key := string(cx.stack[last].Bytes) + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + err := cx.appDeleteGlobalKey(key) + if err != nil { + cx.err = err + return + } + cx.stack = cx.stack[:last] +} + +func opAssetHoldingGet(cx *evalContext) { + last := len(cx.stack) - 1 // asset id + prev := last - 1 // account offset + + assetID := cx.stack[last].Uint + accountIdx := cx.stack[prev].Uint + fieldIdx := uint64(cx.program[cx.pc+1]) + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + cx.err = err + return + } + + var exist uint64 = 0 + var value stackValue + if holding, err := cx.Ledger.AssetHolding(addr, basics.AssetIndex(assetID)); err == nil { + // the holding exist, read the value + exist = 1 + value, err = cx.assetHoldingEnumToValue(&holding, fieldIdx) + if err != nil { + cx.err = err + return + } + } + + cx.stack[prev] = value + cx.stack[last].Uint = exist + + cx.nextpc = cx.pc + 2 +} + +func opAssetParamsGet(cx *evalContext) { + last := len(cx.stack) - 1 // asset id + prev := last - 1 // account offset + + assetID := cx.stack[last].Uint + accountIdx := cx.stack[prev].Uint + paramIdx := uint64(cx.program[cx.pc+1]) + + if cx.Ledger == nil { + cx.err = fmt.Errorf("ledger not available") + return + } + + addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) + if err != nil { + cx.err = err + return + } + + var exist uint64 = 0 + var value stackValue + if params, err := cx.Ledger.AssetParams(addr, basics.AssetIndex(assetID)); err == nil { + // params exist, read the value + exist = 1 + value, err = cx.assetParamsEnumToValue(¶ms, paramIdx) + if err != nil { + cx.err = err + return + } + } + + cx.stack[prev] = value + cx.stack[last].Uint = exist + + cx.nextpc = cx.pc + 2 +} diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go new file mode 100644 index 0000000000..e12c71833d --- /dev/null +++ b/data/transactions/logic/evalStateful_test.go @@ -0,0 +1,2647 @@ +// 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 . + +package logic + +import ( + "encoding/hex" + "fmt" + "math/rand" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" +) + +type balanceRecord struct { + addr basics.Address + balance uint64 + apps map[basics.AppIndex]map[string]basics.TealValue + holdings map[uint64]basics.AssetHolding + assets map[uint64]basics.AssetParams +} + +type testLedger struct { + balances map[basics.Address]balanceRecord + applications map[basics.AppIndex]map[string]basics.TealValue + localCount int + globalCount int + appID uint64 +} + +func makeBalanceRecord(addr basics.Address, balance uint64) balanceRecord { + br := balanceRecord{ + addr: addr, + balance: balance, + apps: make(map[basics.AppIndex]map[string]basics.TealValue), + holdings: make(map[uint64]basics.AssetHolding), + assets: make(map[uint64]basics.AssetParams), + } + return br +} + +func makeTestLedger(balances map[basics.Address]uint64) *testLedger { + l := new(testLedger) + l.balances = make(map[basics.Address]balanceRecord) + for addr, balance := range balances { + l.balances[addr] = makeBalanceRecord(addr, balance) + } + l.applications = make(map[basics.AppIndex]map[string]basics.TealValue) + return l +} + +func (l *testLedger) resetCounters() { + l.localCount = 0 + l.globalCount = 0 +} + +func (l *testLedger) newApp(addr basics.Address, appID uint64) { + l.appID = appID + appIdx := basics.AppIndex(appID) + l.applications[appIdx] = make(map[string]basics.TealValue) + br, ok := l.balances[addr] + if !ok { + br = makeBalanceRecord(addr, 0) + } + br.apps[appIdx] = make(map[string]basics.TealValue) + l.balances[addr] = br +} + +func (l *testLedger) setAsset(addr basics.Address, assetID uint64, params basics.AssetParams) { + br, ok := l.balances[addr] + if !ok { + br = makeBalanceRecord(addr, 0) + } + br.assets[assetID] = params + l.balances[addr] = br +} + +func (l *testLedger) setHolding(addr basics.Address, assetID uint64, holding basics.AssetHolding) { + br, ok := l.balances[addr] + if !ok { + br = makeBalanceRecord(addr, 0) + } + br.holdings[assetID] = holding + l.balances[addr] = br +} + +func (l *testLedger) Round() basics.Round { + return basics.Round(rand.Uint32() + 1) +} + +func (l *testLedger) LatestTimestamp() int64 { + return int64(rand.Uint32() + 1) +} + +func (l *testLedger) Balance(addr basics.Address) (amount basics.MicroAlgos, err error) { + if l.balances == nil { + err = fmt.Errorf("empty ledger") + return + } + br, ok := l.balances[addr] + if !ok { + err = fmt.Errorf("no such address") + return + } + return basics.MicroAlgos{Raw: br.balance}, nil +} + +func (l *testLedger) AppLocalState(addr basics.Address, appIdx basics.AppIndex) (basics.TealKeyValue, error) { + l.localCount++ + if appIdx == 0 { + appIdx = basics.AppIndex(l.appID) + } + if br, ok := l.balances[addr]; ok { + if state, ok := br.apps[appIdx]; ok { + return state, nil + } + return nil, fmt.Errorf("No app for account") + } + return nil, fmt.Errorf("no such address") +} + +func (l *testLedger) AppGlobalState(appIdx basics.AppIndex) (basics.TealKeyValue, error) { + l.globalCount++ + if appIdx == 0 { + appIdx = basics.AppIndex(l.appID) + } + if state, ok := l.applications[appIdx]; ok { + return state, nil + } + return nil, fmt.Errorf("no such app") +} + +func (l *testLedger) AssetHolding(addr basics.Address, assetID basics.AssetIndex) (basics.AssetHolding, error) { + if br, ok := l.balances[addr]; ok { + if asset, ok := br.holdings[uint64(assetID)]; ok { + return asset, nil + } + return basics.AssetHolding{}, fmt.Errorf("No asset for account") + } + return basics.AssetHolding{}, fmt.Errorf("no such address") +} + +func (l *testLedger) AssetParams(addr basics.Address, assetID basics.AssetIndex) (basics.AssetParams, error) { + if br, ok := l.balances[addr]; ok { + if asset, ok := br.assets[uint64(assetID)]; ok { + return asset, nil + } + return basics.AssetParams{}, fmt.Errorf("No asset for account") + } + return basics.AssetParams{}, fmt.Errorf("no such address") +} + +func (l *testLedger) ApplicationID() basics.AppIndex { + return basics.AppIndex(l.appID) +} + +func TestEvalModes(t *testing.T) { + t.Parallel() + // ed25519verify and err are tested separately below + + // check modeAny (TEAL v1 + txna/gtxna) are available in RunModeSignature + // check all opcodes available in runModeApplication + opcodesRunModeAny := `intcblock 0 1 1 1 1 5 100 + bytecblock 0x414c474f 0x1337 0x2001 0xdeadbeef 0x70077007 +bytec 0 +sha256 +keccak256 +sha512_256 +len +intc_0 ++ +intc_1 +- +intc_2 +/ +intc_3 +* +intc 4 +< +intc_1 +> +intc_1 +<= +intc_1 +>= +intc_1 +&& +intc_1 +|| +bytec_1 +bytec_2 +!= +bytec_3 +bytec 4 +== +! +itob +btoi +% // use values left after bytes comparison +| +intc_1 +& +txn Fee +^ +global MinTxnFee +~ +gtxn 0 LastValid +mulw +pop +store 0 +load 0 +bnz label +label: +dup +pop +txna Accounts 0 +gtxna 0 ApplicationArgs 0 +== +` + opcodesRunModeSignature := `arg_0 +arg_1 +!= +arg_2 +arg_3 +!= +&& +txn Sender +arg 4 +!= +&& +!= +&& +` + + opcodesRunModeApplication := `int 0 +balance +&& +intc_0 +intc 6 // 100 +app_opted_in +&& +intc_0 +bytec_0 // ALGO +intc_1 +app_local_put +bytec_0 +intc_1 +app_global_put +intc_0 +intc 6 +bytec_0 +app_local_get_ex +pop +&& +int 0 +bytec_0 +app_global_get_ex +pop +&& +intc_0 +bytec_0 +app_local_del +bytec_0 +app_global_del +intc_0 +intc 5 // 5 +asset_holding_get AssetBalance +pop +&& +intc_0 +intc 5 +asset_params_get AssetTotal +pop +&& +!= +` + type desc struct { + source string + eval func([]byte, EvalParams) (bool, error) + check func([]byte, EvalParams) (int, error) + } + tests := map[runMode]desc{ + runModeSignature: { + source: opcodesRunModeAny + opcodesRunModeSignature, + eval: func(program []byte, ep EvalParams) (bool, error) { return Eval(program, ep) }, + check: func(program []byte, ep EvalParams) (int, error) { return Check(program, ep) }, + }, + runModeApplication: { + source: opcodesRunModeAny + opcodesRunModeApplication, + eval: func(program []byte, ep EvalParams) (bool, error) { + pass, _, err := EvalStateful(program, ep) + return pass, err + }, + check: func(program []byte, ep EvalParams) (int, error) { return CheckStateful(program, ep) }, + }, + } + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + txn.Lsig.Args = [][]byte{ + txn.Txn.Sender[:], + txn.Txn.Receiver[:], + txn.Txn.CloseRemainderTo[:], + txn.Txn.VotePK[:], + txn.Txn.SelectionPK[:], + txn.Txn.Note, + } + params := basics.AssetParams{ + Total: 1000, + Decimals: 2, + DefaultFrozen: false, + UnitName: "ALGO", + AssetName: "", + URL: string(protocol.PaymentTx), + Manager: txn.Txn.Sender, + Reserve: txn.Txn.Receiver, + Freeze: txn.Txn.Receiver, + Clawback: txn.Txn.Receiver, + } + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ledger.newApp(txn.Txn.Sender, 100) + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + ledger.setAsset(txn.Txn.Receiver, 5, params) + ledger.setHolding(txn.Txn.Sender, 5, basics.AssetHolding{Amount: 123, Frozen: true}) + + for mode, test := range tests { + t.Run(fmt.Sprintf("opcodes_mode=%d", mode), func(t *testing.T) { + program, err := AssembleString(test.source) + require.NoError(t, err) + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + ep.Ledger = ledger + ep.Txn.Txn.ApplicationID = 100 + _, err = test.check(program, ep) + require.NoError(t, err) + _, err = test.eval(program, ep) + if err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + }) + } + + // check err opcode work in both modes + for mode, test := range tests { + t.Run(fmt.Sprintf("err_mode=%d", mode), func(t *testing.T) { + source := "err" + program, err := AssembleString(source) + require.NoError(t, err) + ep := defaultEvalParams(nil, nil) + _, err = test.check(program, ep) + require.NoError(t, err) + _, err = test.eval(program, ep) + require.Error(t, err) + require.NotContains(t, err.Error(), "not allowed in current mode") + require.Contains(t, err.Error(), "err opcode") + }) + } + + // check ed25519verify and arg are not allowed in statefull mode + disallowed := []string{ + "byte 0x01\nbyte 0x01\nbyte 0x01\ned25519verify", + "arg 0", + "arg_0", + "arg_1", + "arg_2", + "arg_3", + } + for _, source := range disallowed { + program, err := AssembleString(source) + require.NoError(t, err) + ep := defaultEvalParams(nil, nil) + _, err = CheckStateful(program, ep) + require.Error(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + } + + // check new opcodes are not allowed in stateless mode + newOpcodeCalls := []string{ + "int 0\nbalance", + "int 0\nint 0\napp_opted_in", + "int 0\nint 0\nbyte 0x01\napp_local_get_ex", + "byte 0x01\napp_global_get", + "int 0\nbyte 0x01\napp_global_get_ex", + "int 1\nbyte 0x01\nbyte 0x01\napp_local_put", + "byte 0x01\nint 0\napp_global_put", + "int 0\nbyte 0x01\napp_local_del", + "byte 0x01\napp_global_del", + "int 0\nint 0\nasset_holding_get AssetFrozen", + "int 0\nint 0\nasset_params_get AssetManager", + } + + for _, source := range newOpcodeCalls { + program, err := AssembleString(source) + require.NoError(t, err) + ep := defaultEvalParams(nil, nil) + _, err = Check(program, ep) + require.Error(t, err) + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + } + + require.Equal(t, runMode(1), runModeSignature) + require.Equal(t, runMode(2), runModeApplication) + require.True(t, modeAny == runModeSignature|runModeApplication) + require.True(t, modeAny.Any()) +} + +func TestBalance(t *testing.T) { + t.Parallel() + + text := `int 2 +balance +int 1 +==` + program, err := AssembleString(text) + require.NoError(t, err) + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.TxnGroup = txgroup + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ep.Ledger = makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Receiver: 1, + }, + ) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot load account") + + text = `int 1 +balance +int 1 +==` + program, err = AssembleString(text) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `int 0 +balance +int 1 +==` + program, err = AssembleString(text) + require.NoError(t, err) + var addr basics.Address + copy(addr[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui02")) + ep.Ledger = makeTestLedger( + map[basics.Address]uint64{ + addr: 1, + }, + ) + pass, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch balance") + require.False(t, pass) + + ep.Ledger = makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +func TestAppCheckOptedIn(t *testing.T) { + t.Parallel() + + text := `int 2 // account idx +int 100 // app idx +app_opted_in +int 1 +==` + program, err := AssembleString(text) + require.NoError(t, err) + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.TxnGroup = txgroup + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ep.Ledger = makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Receiver: 1, + }, + ) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot load account") + + // Receiver is not opted in + text = `int 1 // account idx +int 100 // app idx +app_opted_in +int 0 +==` + program, err = AssembleString(text) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // Sender is not opted in + text = `int 0 // account idx +int 100 // app idx +app_opted_in +int 0 +==` + program, err = AssembleString(text) + require.NoError(t, err) + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // Receiver opted in + text = `int 1 // account idx +int 100 // app idx +app_opted_in +int 1 +==` + ledger.newApp(txn.Txn.Receiver, 100) + + program, err = AssembleString(text) + require.NoError(t, err) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // Sender opted in + text = `int 0 // account idx +int 100 // app idx +app_opted_in +int 1 +==` + ledger.newApp(txn.Txn.Sender, 100) + + program, err = AssembleString(text) + require.NoError(t, err) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + +} + +func TestAppReadLocalState(t *testing.T) { + t.Parallel() + + text := `int 2 // account idx +int 100 // app id +txn ApplicationArgs 0 +app_local_get_ex +bnz exist +int 0 +== +bnz exit +exist: +err +exit: +int 1 +==` + program, err := AssembleString(text) + require.NoError(t, err) + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.Txn.Txn.ApplicationID = 100 + ep.TxnGroup = txgroup + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Receiver: 1, + }, + ) + ep.Ledger = ledger + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot load account") + + text = `int 1 // account idx +int 100 // app id +txn ApplicationArgs 0 +app_local_get_ex +bnz exist +int 0 +== +bnz exit +exist: +err +exit: +int 1` + program, err = AssembleString(text) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch app local state") + + ledger = makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Receiver: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Receiver, 9999) + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch app local state") + + // create the app and check the value from ApplicationArgs[0] (protocol.PaymentTx) does not exist + ledger.newApp(txn.Txn.Receiver, 100) + + pass, _, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `int 1 // account idx +int 100 // app id +txn ApplicationArgs 0 +app_local_get_ex +bnz exist +err +exist: +byte 0x414c474f +==` + ledger.balances[txn.Txn.Receiver].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + + program, err = AssembleString(text) + require.NoError(t, err) + + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check special case account idx == 0 => sender + ledger.newApp(txn.Txn.Sender, 100) + text = `int 0 // account idx +int 100 // app id +txn ApplicationArgs 0 +app_local_get_ex +bnz exist +err +exist: +byte 0x414c474f +==` + + program, err = AssembleString(text) + require.NoError(t, err) + + ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check reading state of other app + ledger.newApp(txn.Txn.Sender, 101) + ledger.newApp(txn.Txn.Sender, 100) + text = `int 0 // account idx +int 101 // app id +txn ApplicationArgs 0 +app_local_get_ex +bnz exist +err +exist: +byte 0x414c474f +==` + + program, err = AssembleString(text) + require.NoError(t, err) + + ledger.balances[txn.Txn.Sender].apps[101][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check app_local_get + text = `int 0 // account idx +txn ApplicationArgs 0 +app_local_get +byte 0x414c474f +==` + + program, err = AssembleString(text) + require.NoError(t, err) + + ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check app_local_get default value + text = `int 0 // account idx +byte 0x414c474f +app_local_get +int 0 +==` + + program, err = AssembleString(text) + require.NoError(t, err) + + ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +func TestAppReadGlobalState(t *testing.T) { + t.Parallel() + + text := `int 0 +txn ApplicationArgs 0 +app_global_get_ex +bnz exist +err +exist: +byte 0x414c474f +== +int 100 +txn ApplicationArgs 0 +app_global_get_ex +bnz exist1 +err +exist1: +byte 0x414c474f +== +&& +txn ApplicationArgs 0 +app_global_get +byte 0x414c474f +== +&& +` + program, err := AssembleString(text) + require.NoError(t, err) + + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.TxnGroup = txgroup + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + + ep.Txn.Txn.ApplicationID = 100 + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch global state") + + // create the app and check the value from ApplicationArgs[0] (protocol.PaymentTx) does not exist + ledger.newApp(txn.Txn.Sender, 100) + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "err opcode") + + ledger.applications[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check app_local_get default value + text = `byte 0x414c474f55 +app_global_get +int 0 +==` + + program, err = AssembleString(text) + require.NoError(t, err) + + ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = ` +byte 0x41414141 +int 4141 +app_global_put +int 100 +byte 0x41414141 +app_global_get_ex +int 4141 +== +pop +` + // check that even during application creation (Txn.ApplicationID == 0) + // we will use the the kvCow if the exact application ID (100) is + // specified in the transaction + program, err = AssembleString(text) + require.NoError(t, err) + + ep.Txn.Txn.ApplicationID = 0 + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +const assetsTestProgram = `int 0 +int 55 +asset_holding_get AssetBalance +! +bnz error +int 123 +== +int 0 +int 55 +asset_holding_get AssetFrozen +! +bnz error +int 1 +== +&& +int 1 +int 55 +asset_params_get AssetTotal +! +bnz error +int 1000 +== +&& +int 1 +int 55 +asset_params_get AssetDecimals +! +bnz error +int 2 +== +&& +int 1 +int 55 +asset_params_get AssetDefaultFrozen +! +bnz error +int 0 +== +&& +int 1 +int 55 +asset_params_get AssetUnitName +! +bnz error +byte 0x414c474f +== +&& +int 1 +int 55 +asset_params_get AssetAssetName +! +bnz error +len +int 0 +== +&& +int 1 +int 55 +asset_params_get AssetURL +! +bnz error +txna ApplicationArgs 0 +== +&& +int 1 +int 55 +asset_params_get AssetMetadataHash +! +bnz error +byte 0x0000000000000000000000000000000000000000000000000000000000000000 +== +&& +int 1 +int 55 +asset_params_get AssetManager +! +bnz error +txna Accounts 0 +== +&& +int 1 +int 55 +asset_params_get AssetReserve +! +bnz error +txna Accounts 1 +== +&& +int 1 +int 55 +asset_params_get AssetFreeze +! +bnz error +txna Accounts 1 +== +&& +int 1 +int 55 +asset_params_get AssetClawback +! +bnz error +txna Accounts 1 +== +&& +bnz ok +error: +err +ok: +int 1 +` + +func TestAssets(t *testing.T) { + t.Parallel() + for _, field := range AssetHoldingFieldNames { + if !strings.Contains(assetsTestProgram, field) { + t.Errorf("TestAssets missing field %v", field) + } + } + for _, field := range AssetParamsFieldNames { + if !strings.Contains(assetsTestProgram, field) { + t.Errorf("TestAssets missing field %v", field) + } + } + + // check generic errors + sources := []string{ + "int 5\nint 55\nasset_holding_get AssetBalance", + "int 5\nint 55\nasset_params_get AssetTotal", + } + for _, source := range sources { + + program, err := AssembleString(source) + require.NoError(t, err) + + txn := makeSampleTxn() + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot load account[5]") + } + + program, err := AssembleString(assetsTestProgram) + require.NoError(t, err) + cost, err := CheckStateful(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + + txn := makeSampleTxn() + sb := strings.Builder{} + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + params := basics.AssetParams{ + Total: 1000, + Decimals: 2, + DefaultFrozen: false, + UnitName: "ALGO", + AssetName: "", + URL: string(protocol.PaymentTx), + Manager: txn.Txn.Sender, + Reserve: txn.Txn.Receiver, + Freeze: txn.Txn.Receiver, + Clawback: txn.Txn.Receiver, + } + ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) + + ep := defaultEvalParams(&sb, &txn) + ep.Ledger = ledger + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err := EvalStateful(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + + // check holdings bool value + source := `int 0 // account idx (txn.Sender) +int 55 +asset_holding_get AssetFrozen +! +bnz error +int 0 +== +bnz ok +error: +err +ok: +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: false}) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check holdings invalid offsets + require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_holding_get"].Opcode, program[8]) + program[9] = 0x02 + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid asset holding field 2") + + // check holdings bool value + source = `int 1 +int 55 +asset_params_get AssetDefaultFrozen +! +bnz error +int 1 +== +bnz ok +error: +err +ok: +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + params.DefaultFrozen = true + ledger.setAsset(txn.Txn.Receiver, 55, params) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + // check holdings invalid offsets + require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, program[7]) + program[8] = 0x20 + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid asset params field 32") + + // check empty string + source = `int 1 // account idx (txn.Accounts[1]) +int 55 +asset_params_get AssetURL +! +bnz error +len +int 0 +== +bnz ok +error: +err +ok: +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + params.URL = "" + ledger.setAsset(txn.Txn.Receiver, 55, params) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + + source = `int 1 +int 55 +asset_params_get AssetURL +! +bnz error +int 0 +== +bnz ok +error: +err +ok: +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + params.URL = "" + ledger.setAsset(txn.Txn.Receiver, 55, params) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot compare ([]byte == uint64)") + require.False(t, pass) +} + +func TestAppLocalReadWriteDeleteErrors(t *testing.T) { + t.Parallel() + + sourceRead := `int 0 // account idx (txn.Sender) +int 100 // app id +byte 0x414c474f // key "ALGO" +app_local_get_ex +! +bnz error +int 0x77 +== +int 0 +int 100 +byte 0x414c474f41 // ALGOA +app_local_get_ex +! +bnz error +int 1 +== +&& +bnz ok +error: +err +ok: +int 1 +` + sourceWrite := `int 0 // account idx (txn.Sender) +byte 0x414c474f // key "ALGO" +int 100 +app_local_put +int 1 +` + sourceDelete := `int 0 // account idx +byte 0x414c474f // key "ALGO" +app_local_del +int 100 +` + type test struct { + source string + accNumOffset int + } + + tests := map[string]test{ + "read": {sourceRead, 20}, + "write": {sourceWrite, 13}, + "delete": {sourceDelete, 12}, + } + for name, test := range tests { + t.Run(fmt.Sprintf("test=%s", name), func(t *testing.T) { + source := test.source + firstCmdOffset := test.accNumOffset + + program, err := AssembleString(source) + require.NoError(t, err) + + txn := makeSampleTxn() + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + ep.Txn.Txn.ApplicationID = 100 + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + + saved := program[firstCmdOffset] + require.Equal(t, opsByName[0]["intc_0"].Opcode, saved) + program[firstCmdOffset] = opsByName[0]["intc_1"].Opcode + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot load account[100]") + + program[firstCmdOffset] = saved + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch app local state") + + ledger.newApp(txn.Txn.Sender, 100) + + if name == "read" { + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "err opcode") // no such key + } + + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.balances[txn.Txn.Sender].apps[100]["ALGOA"] = basics.TealValue{Type: basics.TealUintType, Uint: 1} + + ledger.resetCounters() + pass, delta, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + // for read test: the second call to the state fulfilled from the cache + require.Equal(t, 1, ledger.localCount) + }) + } +} + +func TestAppLocalStateReadWrite(t *testing.T) { + t.Parallel() + + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100) + + // write int and bytes values + source := `int 0 // account +byte 0x414c474f // key "ALGO" +int 0x77 // value +app_local_put +int 0 // account +byte 0x414c474f41 // key "ALGOA" +byte 0x414c474f // value +app_local_put +int 0 // account +int 100 // app id +byte 0x414c474f41 // key "ALGOA" +app_local_get_ex +bnz exist +err +exist: +byte 0x414c474f +== +int 0 // account +int 100 // app id +byte 0x414c474f // key "ALGO" +app_local_get_ex +bnz exist2 +err +exist2: +int 0x77 +== +&& +` + program, err := AssembleString(source) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + + require.Equal(t, 2, len(delta.LocalDeltas[0])) + vd := delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x77), vd.Uint) + + vd = delta.LocalDeltas[0]["ALGOA"] + require.Equal(t, basics.SetBytesAction, vd.Action) + require.Equal(t, "ALGO", vd.Bytes) + + require.Equal(t, 1, ledger.localCount) + + // write same value without writing, expect no local delta + source = `int 0 // account +byte 0x414c474f // key +int 0x77 // value +app_local_put +int 0 // account +int 100 // app id +byte 0x414c474f // key +app_local_get_ex +bnz exist +err +exist: +int 0x77 +== +` + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.localCount) + + // write same value after reading, expect no local delta + source = `int 0 // account +int 100 // app id +byte 0x414c474f // key +app_local_get_ex +bnz exist +err +exist: +int 0 // account +byte 0x414c474f // key +int 0x77 // value +app_local_put +int 0 // account +int 100 // app id +byte 0x414c474f // key +app_local_get_ex +bnz exist2 +err +exist2: +== +` + ledger.resetCounters() + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.localCount) + + // write a value and expect local delta change + source = `int 0 // account +byte 0x414c474f41 // key "ALGOA" +int 0x78 // value +app_local_put +int 1 +` + ledger.resetCounters() + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + require.Equal(t, 1, len(delta.LocalDeltas[0])) + vd = delta.LocalDeltas[0]["ALGOA"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, 1, ledger.localCount) + + // write a value to exising key and expect delta change and reading the new value + source = `int 0 // account +byte 0x414c474f // key "ALGO" +int 0x78 // value +app_local_put +int 0 // account +int 100 // app id +byte 0x414c474f // key "ALGO" +app_local_get_ex +bnz exist +err +exist: +int 0x78 +== +` + ledger.resetCounters() + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + require.Equal(t, 1, len(delta.LocalDeltas[0])) + vd = delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, 1, ledger.localCount) + + // write a value after read and expect delta change + source = `int 0 // account +int 100 // app id +byte 0x414c474f // key "ALGO" +app_local_get_ex +bnz exist +err +exist: +int 0 // account +byte 0x414c474f // key "ALGO" +int 0x78 // value +app_local_put +` + ledger.resetCounters() + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + require.Equal(t, 1, len(delta.LocalDeltas[0])) + vd = delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, 1, ledger.localCount) + + // write a few values and expect delta change only for unique changed + source = `int 0 // account +byte 0x414c474f // key "ALGO" +int 0x77 // value +app_local_put +int 0 // account +byte 0x414c474f // key "ALGO" +int 0x78 // value +app_local_put +int 0 // account +byte 0x414c474f41 // key "ALGOA" +int 0x78 // value +app_local_put +int 1 // account +byte 0x414c474f // key "ALGO" +int 0x79 // value +app_local_put +int 1 +` + ledger.resetCounters() + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + + ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 500) + ledger.balances[txn.Txn.Receiver].apps[100] = make(map[string]basics.TealValue) + + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 2, len(delta.LocalDeltas)) + require.Equal(t, 2, len(delta.LocalDeltas[0])) + require.Equal(t, 1, len(delta.LocalDeltas[1])) + vd = delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + + vd = delta.LocalDeltas[0]["ALGOA"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + + vd = delta.LocalDeltas[1]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x79), vd.Uint) + + require.Equal(t, 2, ledger.localCount) // one call to ledger per account +} + +func TestAppGlobalReadWriteDeleteErrors(t *testing.T) { + t.Parallel() + + sourceRead := `int 0 +byte 0x414c474f // key "ALGO" +app_global_get_ex +bnz ok +err +ok: +int 0x77 +== +` + sourceReadSimple := `byte 0x414c474f // key "ALGO" +app_global_get +int 0x77 +== +` + + sourceWrite := `byte 0x414c474f // key "ALGO" +int 100 +app_global_put +int 1 +` + sourceDelete := `byte 0x414c474f // key "ALGO" +app_global_del +int 1 +` + tests := map[string]string{ + "read": sourceRead, + "reads": sourceReadSimple, + "write": sourceWrite, + "delete": sourceDelete, + } + for name, source := range tests { + t.Run(fmt.Sprintf("test=%s", name), func(t *testing.T) { + program, err := AssembleString(source) + require.NoError(t, err) + + txn := makeSampleTxn() + ep := defaultEvalParams(nil, nil) + ep.Txn = &txn + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + + txn.Txn.ApplicationID = 100 + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch global state") + + ledger.newApp(txn.Txn.Sender, 100) + + // a special test for read + if name == "read" { + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "err opcode") // no such key + } + ledger.applications[100]["ALGO"] = basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + + ledger.resetCounters() + pass, delta, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.globalCount) + }) + } +} + +func TestAppGlobalReadWrite(t *testing.T) { + t.Parallel() + + // check writing ints and bytes + source := `byte 0x414c474f // key "ALGO" +int 0x77 // value +app_global_put +byte 0x414c474f41 // key "ALGOA" +byte 0x414c474f // value +app_global_put +// check simple +byte 0x414c474f41 // key "ALGOA" +app_global_get +byte 0x414c474f +== +// check generic with alias +int 0 // current app id alias +byte 0x414c474f41 // key "ALGOA" +app_global_get_ex +bnz ok +err +ok: +byte 0x414c474f +== +&& +// check generic with exact app id +int 100 // current app id +byte 0x414c474f41 // key "ALGOA" +app_global_get_ex +bnz ok1 +err +ok1: +byte 0x414c474f +== +&& +// check simple +byte 0x414c474f +app_global_get +int 0x77 +== +&& +// check generic with alias +int 0 // current app id +byte 0x414c474f +app_global_get_ex +bnz ok2 +err +ok2: +int 0x77 +== +&& +// check generic with exact app id +int 100 // current app id +byte 0x414c474f +app_global_get_ex +bnz ok3 +err +ok3: +int 0x77 +== +&& +` + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100) + + program, err := AssembleString(source) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err := EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 2, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + + vd := delta.GlobalDelta["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x77), vd.Uint) + + vd = delta.GlobalDelta["ALGOA"] + require.Equal(t, basics.SetBytesAction, vd.Action) + require.Equal(t, "ALGO", vd.Bytes) + + require.Equal(t, 1, ledger.globalCount) + require.Equal(t, 0, ledger.localCount) + + // write existing value before read + source = `byte 0x414c474f // key "ALGO" +int 0x77 // value +app_global_put +byte 0x414c474f +app_global_get +int 0x77 +== +` + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.applications[100]["ALGO"] = algoValue + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.globalCount) + + // write existing value after read + source = `int 0 +byte 0x414c474f +app_global_get_ex +bnz ok +err +ok: +pop +byte 0x414c474f +int 0x77 +app_global_put +byte 0x414c474f +app_global_get +int 0x77 +== +` + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + ledger.applications[100]["ALGO"] = algoValue + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.globalCount) + + // write new values after and before read + source = `int 0 +byte 0x414c474f +app_global_get_ex +bnz ok +err +ok: +pop +byte 0x414c474f +int 0x78 +app_global_put +int 0 +byte 0x414c474f +app_global_get_ex +bnz ok2 +err +ok2: +int 0x78 +== +byte 0x414c474f41 +byte 0x414c474f +app_global_put +int 0 +byte 0x414c474f41 +app_global_get_ex +bnz ok3 +err +ok3: +byte 0x414c474f +== +&& +` + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + ledger.applications[100]["ALGO"] = algoValue + + program, err = AssembleString(source) + require.NoError(t, err) + sb := strings.Builder{} + ep.Trace = &sb + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 2, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.globalCount) + + vd = delta.GlobalDelta["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + + vd = delta.GlobalDelta["ALGOA"] + require.Equal(t, basics.SetBytesAction, vd.Action) + require.Equal(t, "ALGO", vd.Bytes) + + require.Equal(t, 1, ledger.globalCount) + require.Equal(t, 0, ledger.localCount) +} + +func TestAppGlobalReadOtherApp(t *testing.T) { + t.Parallel() + source := `int 101 +byte "mykey1" +app_global_get_ex +bz ok1 +err +ok1: +pop +int 101 +byte "mykey" +app_global_get_ex +bnz ok2 +err +ok2: +byte "myval" +== +` + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100) + + program, err := AssembleString(source) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err := EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to fetch global state for app 101: no such app") + require.False(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + + ledger.newApp(txn.Txn.Receiver, 101) + ledger.newApp(txn.Txn.Receiver, 100) // this keeps current app id = 100 + algoValue := basics.TealValue{Type: basics.TealBytesType, Bytes: "myval"} + ledger.applications[101]["mykey"] = algoValue + + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) +} + +func TestAppGlobalDelete(t *testing.T) { + t.Parallel() + + // check write/delete/read + source := `byte 0x414c474f // key "ALGO" +int 0x77 // value +app_global_put +byte 0x414c474f41 // key "ALGOA" +byte 0x414c474f +app_global_put +byte 0x414c474f +app_global_del +byte 0x414c474f41 +app_global_del +int 0 +byte 0x414c474f +app_global_get_ex +bnz error +int 0 +byte 0x414c474f41 +app_global_get_ex +bnz error +== +bnz ok +error: +err +ok: +int 1 +` + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100) + sb := strings.Builder{} + ep.Trace = &sb + + program, err := AssembleString(source) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err := EvalStateful(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.applications[100]["ALGO"] = algoValue + + // check delete existing + source = `byte 0x414c474f // key "ALGO" +app_global_del +int 100 +byte 0x414c474f +app_global_get_ex +== // two zeros +` + + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 1, len(delta.GlobalDelta)) + vd := delta.GlobalDelta["ALGO"] + require.Equal(t, basics.DeleteAction, vd.Action) + require.Equal(t, uint64(0), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + ledger.applications[100]["ALGO"] = algoValue + + // check delete and write non-existing + source = `byte 0x414c474f41 // key "ALGOA" +app_global_del +int 0 +byte 0x414c474f41 +app_global_get_ex +== // two zeros +byte 0x414c474f41 +int 0x78 +app_global_put +` + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 1, len(delta.GlobalDelta)) + vd = delta.GlobalDelta["ALGOA"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + ledger.applications[100]["ALGO"] = algoValue + + // check delete and write existing + source = `byte 0x414c474f // key "ALGO" +app_global_del +byte 0x414c474f +int 0x78 +app_global_put +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 1, len(delta.GlobalDelta)) + vd = delta.GlobalDelta["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + ledger.applications[100]["ALGO"] = algoValue + + // check delete,write,delete existing + source = `byte 0x414c474f // key "ALGO" +app_global_del +byte 0x414c474f +int 0x78 +app_global_put +byte 0x414c474f +app_global_del +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 1, len(delta.GlobalDelta)) + vd = delta.GlobalDelta["ALGO"] + require.Equal(t, basics.DeleteAction, vd.Action) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.applications[100], "ALGOA") + delete(ledger.applications[100], "ALGO") + + ledger.applications[100]["ALGO"] = algoValue + + // check delete,write,delete non-existing + source = `byte 0x414c474f41 // key "ALGOA" +app_global_del +byte 0x414c474f41 +int 0x78 +app_global_put +byte 0x414c474f41 +app_global_del +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 0, ledger.localCount) + require.Equal(t, 1, ledger.globalCount) +} + +func TestAppLocalDelete(t *testing.T) { + t.Parallel() + + // check write/delete/read + source := `int 0 // account +byte 0x414c474f // key "ALGO" +int 0x77 // value +app_local_put +int 1 +byte 0x414c474f41 // key "ALGOA" +byte 0x414c474f +app_local_put +int 0 +byte 0x414c474f +app_local_del +int 1 +byte 0x414c474f41 +app_local_del +int 0 +int 0 +byte 0x414c474f +app_local_get_ex +bnz error +int 1 +int 100 +byte 0x414c474f41 +app_local_get_ex +bnz error +== +bnz ok +error: +err +ok: +int 1 +` + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txn.Txn.ApplicationID = 100 + ep.Txn = &txn + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + ep.Ledger = ledger + ledger.newApp(txn.Txn.Sender, 100) + ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 1) + ledger.balances[txn.Txn.Receiver].apps[100] = make(basics.TealKeyValue) + + sb := strings.Builder{} + ep.Trace = &sb + + program, err := AssembleString(source) + require.NoError(t, err) + cost, err := CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err := EvalStateful(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 2, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + delete(ledger.balances[txn.Txn.Receiver].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Receiver].apps[100], "ALGO") + + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + // check delete existing + source = `int 0 // account +byte 0x414c474f // key "ALGO" +app_local_del +int 0 +int 100 +byte 0x414c474f +app_local_get_ex +== // two zeros +` + + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + vd := delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.DeleteAction, vd.Action) + require.Equal(t, uint64(0), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 1, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + // check delete and write non-existing + source = `int 0 // account +byte 0x414c474f41 // key "ALGOA" +app_local_del +int 0 +int 0 +byte 0x414c474f41 +app_local_get_ex +== // two zeros +int 0 +byte 0x414c474f41 +int 0x78 +app_local_put +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + vd = delta.LocalDeltas[0]["ALGOA"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 1, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + // check delete and write existing + source = `int 0 // account +byte 0x414c474f // key "ALGO" +app_local_del +int 0 +byte 0x414c474f +int 0x78 +app_local_put +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + vd = delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.SetUintAction, vd.Action) + require.Equal(t, uint64(0x78), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 1, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + // check delete,write,delete existing + source = `int 0 // account +byte 0x414c474f // key "ALGO" +app_local_del +int 0 +byte 0x414c474f +int 0x78 +app_local_put +int 0 +byte 0x414c474f +app_local_del +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 1, len(delta.LocalDeltas)) + vd = delta.LocalDeltas[0]["ALGO"] + require.Equal(t, basics.DeleteAction, vd.Action) + require.Equal(t, uint64(0), vd.Uint) + require.Equal(t, "", vd.Bytes) + require.Equal(t, 1, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) + + ledger.resetCounters() + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") + delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGO") + + ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue + + // check delete,write,delete non-existing + source = `int 0 // account +byte 0x414c474f41 // key "ALGOA" +app_local_del +int 0 +byte 0x414c474f41 +int 0x78 +app_local_put +int 0 +byte 0x414c474f41 +app_local_del +int 1 +` + program, err = AssembleString(source) + require.NoError(t, err) + cost, err = CheckStateful(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, delta, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + require.Equal(t, 0, len(delta.GlobalDelta)) + require.Equal(t, 0, len(delta.LocalDeltas)) + require.Equal(t, 1, ledger.localCount) + require.Equal(t, 0, ledger.globalCount) +} + +func TestEnumFieldErrors(t *testing.T) { + ep := defaultEvalParams(nil, nil) + + source := `txn Amount` + origTxnType := TxnFieldTypes[Amount] + TxnFieldTypes[Amount] = StackBytes + defer func() { + TxnFieldTypes[Amount] = origTxnType + }() + + program, err := AssembleString(source) + require.NoError(t, err) + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "Amount expected field type is []byte but got uint64") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "Amount expected field type is []byte but got uint64") + + source = `global MinTxnFee` + origGlobalType := GlobalFieldTypes[MinTxnFee] + GlobalFieldTypes[MinTxnFee] = StackBytes + defer func() { + GlobalFieldTypes[MinTxnFee] = origGlobalType + }() + + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "MinTxnFee expected field type is []byte but got uint64") + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "MinTxnFee expected field type is []byte but got uint64") + + txn := makeSampleTxn() + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + params := basics.AssetParams{ + Total: 1000, + Decimals: 2, + DefaultFrozen: false, + UnitName: "ALGO", + AssetName: "", + URL: string(protocol.PaymentTx), + Manager: txn.Txn.Sender, + Reserve: txn.Txn.Receiver, + Freeze: txn.Txn.Receiver, + Clawback: txn.Txn.Receiver, + } + ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) + + ep.Txn = &txn + ep.Ledger = ledger + + source = `int 0 +int 55 +asset_holding_get AssetBalance +pop +` + origAssetHoldingType := AssetHoldingFieldTypes[AssetBalance] + AssetHoldingFieldTypes[AssetBalance] = StackBytes + defer func() { + AssetHoldingFieldTypes[AssetBalance] = origAssetHoldingType + }() + + program, err = AssembleString(source) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "AssetBalance expected field type is []byte but got uint64") + + source = `int 1 +int 55 +asset_params_get AssetTotal +pop +` + origAssetTotalType := AssetParamsFieldTypes[AssetTotal] + AssetParamsFieldTypes[AssetTotal] = StackBytes + defer func() { + AssetParamsFieldTypes[AssetTotal] = origAssetTotalType + }() + + program, err = AssembleString(source) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "AssetTotal expected field type is []byte but got uint64") +} + +func TestReturnTypes(t *testing.T) { + // Ensure all opcodes return values they supposed to according to the OpSpecs table + t.Parallel() + typeToArg := map[StackType]string{ + StackUint64: "int 1\n", + StackAny: "int 1\n", + StackBytes: "byte 0x33343536\n", + } + ep := defaultEvalParams(nil, nil) + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + ep.Txn = &txn + ep.TxnGroup = txgroup + ep.Txn.Txn.ApplicationID = 1 + txn.Lsig.Args = [][]byte{ + []byte("aoeu"), + []byte("aoeu"), + []byte("aoeu2"), + []byte("aoeu3"), + } + ledger := makeTestLedger( + map[basics.Address]uint64{ + txn.Txn.Sender: 1, + }, + ) + params := basics.AssetParams{ + Total: 1000, + Decimals: 2, + DefaultFrozen: false, + UnitName: "ALGO", + AssetName: "", + URL: string(protocol.PaymentTx), + Manager: txn.Txn.Sender, + Reserve: txn.Txn.Receiver, + Freeze: txn.Txn.Receiver, + Clawback: txn.Txn.Receiver, + } + ledger.setAsset(txn.Txn.Receiver, 1, params) + ledger.setHolding(txn.Txn.Sender, 1, basics.AssetHolding{Amount: 123, Frozen: true}) + ledger.newApp(txn.Txn.Sender, 1) + ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 1) + ledger.balances[txn.Txn.Receiver].apps[1] = make(basics.TealKeyValue) + key, err := hex.DecodeString("33343536") + require.NoError(t, err) + algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} + ledger.balances[txn.Txn.Receiver].apps[1][string(key)] = algoValue + + ep.Ledger = ledger + + specialCmd := map[string]string{ + "txn": "txn Sender", + "txna": "txna ApplicationArgs 0", + "gtxn": "gtxn 0 Sender", + "gtxna": "gtxna 0 ApplicationArgs 0", + "global": "global MinTxnFee", + "arg": "arg 0", + "load": "load 0", + "store": "store 0", + "intc": "intcblock 0\nintc 0", + "intc_0": "intcblock 0\nintc_0", + "intc_1": "intcblock 0 0\nintc_1", + "intc_2": "intcblock 0 0 0\nintc_2", + "intc_3": "intcblock 0 0 0 0\nintc_3", + "bytec": "bytecblock 0x32\nbytec 0", + "bytec_0": "bytecblock 0x32\nbytec_0", + "bytec_1": "bytecblock 0x32 0x33\nbytec_1", + "bytec_2": "bytecblock 0x32 0x33 0x34\nbytec_2", + "bytec_3": "bytecblock 0x32 0x33 0x34 0x35\nbytec_3", + "substring": "substring 0 2", + "ed25519verify": "pop\npop\npop\nint 1", // ignore + "asset_params_get": "asset_params_get AssetTotal", + "asset_holding_get": "asset_holding_get AssetBalance", + } + + byName := opsByName[LogicVersion] + for _, m := range []runMode{runModeSignature, runModeApplication} { + t.Run(fmt.Sprintf("m=%s", m.String()), func(t *testing.T) { + for name, spec := range byName { + if len(spec.Returns) == 0 || (m&spec.Modes) == 0 { + continue + } + var sb strings.Builder + sb.Grow(64) + for _, t := range spec.Args { + sb.WriteString(typeToArg[t]) + } + if cmd, ok := specialCmd[name]; ok { + sb.WriteString(cmd + "\n") + } else { + sb.WriteString(name + "\n") + } + source := sb.String() + program, err := AssembleString(source) + require.NoError(t, err) + + var cx evalContext + cx.EvalParams = ep + cx.runModeFlags = m + if m == runModeApplication { + cx.appEvalDelta = basics.EvalDelta{ + GlobalDelta: make(basics.StateDelta), + LocalDeltas: make(map[uint64]basics.StateDelta), + } + cx.globalStateCow = nil + cx.readOnlyGlobalStates = make(map[uint64]basics.TealKeyValue) + cx.localStateCows = make(map[basics.Address]*indexedCow) + cx.readOnlyLocalStates = make(map[ckey]basics.TealKeyValue) + } + + eval(program, &cx) + + require.Equal( + t, + len(spec.Returns), len(cx.stack), + fmt.Sprintf("%s expected to return %d values but stack has %d", spec.Name, len(spec.Returns), len(cx.stack)), + ) + for i := 0; i < len(spec.Returns); i++ { + sp := len(cx.stack) - 1 - i + stackType := cx.stack[sp].argType() + retType := spec.Returns[i] + require.True( + t, typecheck(retType, stackType), + fmt.Sprintf("%s expected to return %s but actual is %s", spec.Name, retType.String(), stackType.String()), + ) + } + } + }) + } +} + +func TestRound(t *testing.T) { + source := `global Round +int 1 +>= +` + ledger := makeTestLedger( + map[basics.Address]uint64{}, + ) + program, err := AssembleString(source) + require.NoError(t, err) + + ep := defaultEvalParams(nil, nil) + _, err = CheckStateful(program, ep) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + require.False(t, pass) + + ep.Ledger = ledger + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +func TestLatestTimestamp(t *testing.T) { + source := `global LatestTimestamp +int 1 +>= +` + ledger := makeTestLedger( + map[basics.Address]uint64{}, + ) + program, err := AssembleString(source) + require.NoError(t, err) + + ep := defaultEvalParams(nil, nil) + _, err = CheckStateful(program, ep) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + require.False(t, pass) + + ep.Ledger = ledger + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index c314879afe..475649ce27 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -22,6 +22,7 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "strconv" "strings" "testing" @@ -36,12 +37,30 @@ import ( "github.com/algorand/go-algorand/protocol" ) +// Note that most of the tests use defaultEvalProto/defaultEvalParams as evaluator version so that +// we check that TEAL v1 and v2 programs are compatible with the latest evaluator func defaultEvalProto() config.ConsensusParams { - return config.ConsensusParams{LogicSigVersion: 1, LogicSigMaxCost: 20000} + return defaultEvalProtoWithVersion(LogicVersion) +} + +func defaultEvalProtoV1() config.ConsensusParams { + return defaultEvalProtoWithVersion(1) +} + +func defaultEvalProtoWithVersion(version uint64) config.ConsensusParams { + return config.ConsensusParams{LogicSigVersion: version, LogicSigMaxCost: 20000} +} + +func defaultEvalParamsV1(sb *strings.Builder, txn *transactions.SignedTxn) EvalParams { + return defaultEvalParamsWithVersion(sb, txn, 1) } func defaultEvalParams(sb *strings.Builder, txn *transactions.SignedTxn) EvalParams { - proto := defaultEvalProto() + return defaultEvalParamsWithVersion(sb, txn, LogicVersion) +} + +func defaultEvalParamsWithVersion(sb *strings.Builder, txn *transactions.SignedTxn, version uint64) EvalParams { + proto := defaultEvalProtoWithVersion(version) var pt *transactions.SignedTxn if txn != nil { @@ -51,78 +70,109 @@ func defaultEvalParams(sb *strings.Builder, txn *transactions.SignedTxn) EvalPar pt = &at } - if sb == nil { // have to do this since go's nil semantics: https://golang.org/doc/faq#nil_error - return EvalParams{Proto: &proto, Txn: pt} + ep := EvalParams{} + ep.Proto = &proto + ep.Txn = pt + if sb != nil { // have to do this since go's nil semantics: https://golang.org/doc/faq#nil_error + ep.Trace = sb } - - return EvalParams{Proto: &proto, Trace: sb, Txn: pt} + return ep } func TestTooManyArgs(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1`) - require.NoError(t, err) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - args := [transactions.EvalMaxArgs + 1][]byte{} - txn.Lsig.Args = args[:] - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + args := [transactions.EvalMaxArgs + 1][]byte{} + txn.Lsig.Args = args[:] + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + require.Error(t, err) + require.False(t, pass) + }) + } +} + +func TestEmptyProgram(t *testing.T) { + t.Parallel() + pass, err := Eval(nil, defaultEvalParams(nil, nil)) require.Error(t, err) + require.Contains(t, err.Error(), "invalid program (empty)") require.False(t, pass) } func TestWrongProtoVersion(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1`) - require.NoError(t, err) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - sb := strings.Builder{} - proto := defaultEvalProto() - proto.LogicSigVersion = 0 - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn}) - require.Error(t, err) - require.False(t, pass) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + sb := strings.Builder{} + proto := defaultEvalProto() + proto.LogicSigVersion = 0 + ep := defaultEvalParams(&sb, &txn) + ep.Proto = &proto + _, err = Check(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "LogicSig not supported") + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "LogicSig not supported") + require.False(t, pass) + }) + } } func TestTrivialMath(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 2 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 2 int 3 + int 5 -==`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - pass, err := Eval(program, defaultEvalParams(nil, &txn)) - require.True(t, pass) - require.NoError(t, err) +==`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + pass, err := Eval(program, defaultEvalParams(nil, &txn)) + require.True(t, pass) + require.NoError(t, err) + }) + } } func TestSha256EqArg(t *testing.T) { t.Parallel() - program, err := AssembleString(`arg 0 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`arg 0 sha256 byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= -==`) - require.NoError(t, err) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = [][]byte{[]byte("=0\x97S\x85H\xe9\x91B\xfd\xdb;1\xf5Z\xaec?\xae\xf2I\x93\x08\x12\x94\xaa~\x06\x08\x849b")} - sb := strings.Builder{} - proto := defaultEvalProto() - ep := EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - cost, err := Check(program, ep) - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := Eval(program, ep) - require.True(t, pass) - require.NoError(t, err) +==`, 1) + require.NoError(t, err) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{[]byte("=0\x97S\x85H\xe9\x91B\xfd\xdb;1\xf5Z\xaec?\xae\xf2I\x93\x08\x12\x94\xaa~\x06\x08\x849b")} + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + cost, err := Check(program, ep) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, err := Eval(program, ep) + require.True(t, pass) + require.NoError(t, err) + }) + } } const tlhcProgramText = `txn CloseRemainderTo @@ -161,186 +211,212 @@ int 1000000 func TestTLHC(t *testing.T) { t.Parallel() - a1, _ := basics.UnmarshalChecksumAddress("DFPKC2SJP3OTFVJFMCD356YB7BOT4SJZTGWLIPPFEWL3ZABUFLTOY6ILYE") - a2, _ := basics.UnmarshalChecksumAddress("YYKRMERAFXMXCDWMBNR6BUUWQXDCUR53FPUGXLUYS7VNASRTJW2ENQ7BMQ") - secret, _ := base64.StdEncoding.DecodeString("xPUB+DJir1wsH7g2iEY1QwYqHqYH1vUJtzZKW4RxXsY=") - program, err := AssembleString(tlhcProgramText) - require.NoError(t, err) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - // right answer - txn.Lsig.Args = [][]byte{secret} - txn.Txn.FirstValid = 999999 - sb := strings.Builder{} - block := bookkeeping.Block{} - proto := defaultEvalProto() - ep := EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - cost, err := Check(program, ep) - if err != nil { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := Eval(program, ep) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.False(t, pass) - isNotPanic(t, err) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { - txn.Txn.Receiver = a2 - txn.Txn.CloseRemainderTo = a2 - sb = strings.Builder{} - ep = EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - pass, err = Eval(program, ep) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) + a1, _ := basics.UnmarshalChecksumAddress("DFPKC2SJP3OTFVJFMCD356YB7BOT4SJZTGWLIPPFEWL3ZABUFLTOY6ILYE") + a2, _ := basics.UnmarshalChecksumAddress("YYKRMERAFXMXCDWMBNR6BUUWQXDCUR53FPUGXLUYS7VNASRTJW2ENQ7BMQ") + secret, _ := base64.StdEncoding.DecodeString("xPUB+DJir1wsH7g2iEY1QwYqHqYH1vUJtzZKW4RxXsY=") + program, err := AssembleStringWithVersion(tlhcProgramText, v) + require.NoError(t, err) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + // right answer + txn.Lsig.Args = [][]byte{secret} + txn.Txn.FirstValid = 999999 + sb := strings.Builder{} + block := bookkeeping.Block{} + ep := defaultEvalParams(&sb, &txn) + cost, err := Check(program, ep) + if err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, cost < 1000) + pass, err := Eval(program, ep) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) - txn.Txn.Receiver = a2 - txn.Txn.CloseRemainderTo = a2 - sb = strings.Builder{} - txn.Txn.FirstValid = 1 - ep = EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - pass, err = Eval(program, ep) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.False(t, pass) - isNotPanic(t, err) + txn.Txn.Receiver = a2 + txn.Txn.CloseRemainderTo = a2 + sb = strings.Builder{} + ep = defaultEvalParams(&sb, &txn) + pass, err = Eval(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) - txn.Txn.Receiver = a1 - txn.Txn.CloseRemainderTo = a1 - sb = strings.Builder{} - txn.Txn.FirstValid = 999999 - ep = EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - pass, err = Eval(program, ep) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) + txn.Txn.Receiver = a2 + txn.Txn.CloseRemainderTo = a2 + sb = strings.Builder{} + txn.Txn.FirstValid = 1 + ep = defaultEvalParams(&sb, &txn) + pass, err = Eval(program, ep) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) - // wrong answer - txn.Lsig.Args = [][]byte{[]byte("=0\x97S\x85H\xe9\x91B\xfd\xdb;1\xf5Z\xaec?\xae\xf2I\x93\x08\x12\x94\xaa~\x06\x08\x849a")} - sb = strings.Builder{} - block.BlockHeader.Round = 1 - ep = EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} - pass, err = Eval(program, ep) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + txn.Txn.Receiver = a1 + txn.Txn.CloseRemainderTo = a1 + sb = strings.Builder{} + txn.Txn.FirstValid = 999999 + ep = defaultEvalParams(&sb, &txn) + pass, err = Eval(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) + + // wrong answer + txn.Lsig.Args = [][]byte{[]byte("=0\x97S\x85H\xe9\x91B\xfd\xdb;1\xf5Z\xaec?\xae\xf2I\x93\x08\x12\x94\xaa~\x06\x08\x849a")} + sb = strings.Builder{} + block.BlockHeader.Round = 1 + ep = defaultEvalParams(&sb, &txn) + pass, err = Eval(program, ep) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestU64Math(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x1234567812345678 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x1234567812345678 int 0x100000000 / int 0x12345678 -==`) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==`, v) + require.NoError(t, err) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) + }) } - require.True(t, pass) - require.NoError(t, err) } func TestItob(t *testing.T) { t.Parallel() - program, err := AssembleString(`byte 0x1234567812345678 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`byte 0x1234567812345678 int 0x1234567812345678 itob -==`) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==`, v) + require.NoError(t, err) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestBtoi(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x1234567812345678 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x1234567812345678 byte 0x1234567812345678 btoi -==`) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==`, v) + require.NoError(t, err) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestBtoiTooLong(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x1234567812345678 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x1234567812345678 byte 0x1234567812345678aaaa btoi -==`) - require.NoError(t, err) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==`, v) + require.NoError(t, err) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestBnz(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 dup bnz safe err safe: int 1 -+`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, pass) -} - -func TestBnz2(t *testing.T) { - t.Parallel() - program, err := AssembleString(`int 1 -int 2 -int 1 -int 2 ++`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestBnz2(t *testing.T) { + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 +int 2 +int 1 +int 2 > bnz planb * @@ -351,85 +427,196 @@ planb: after: dup pop -`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestBz(t *testing.T) { + t.Parallel() + for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0 +dup +bz safe +err +safe: +int 1 ++`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestB(t *testing.T) { + t.Parallel() + for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`b safe +err +safe: +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestReturn(t *testing.T) { + t.Parallel() + for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 +return +err`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestReturnFalse(t *testing.T) { + t.Parallel() + for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0 +return +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.NoError(t, err) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestSubUnderflow(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 int 0x100000000 - pop -int 1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestAddOverflow(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0xf000000000000000 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0xf000000000000000 int 0x1111111111111111 + pop -int 1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestMulOverflow(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x111111111 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x111111111 int 0x222222222 * pop -int 1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestMulwImpl(t *testing.T) { @@ -458,7 +645,9 @@ func TestMulwImpl(t *testing.T) { func TestMulw(t *testing.T) { t.Parallel() // multiply two numbers, ensure high is 2 and low is 0x468acf130eca8642 - program, err := AssembleString(`int 0x111111111 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x111111111 int 0x222222222 mulw int 0x468acf130eca8642 // compare low (top of the stack) @@ -472,91 +661,107 @@ bnz done err done: int 1 // ret 1 -`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) + }) } - require.True(t, pass) - require.NoError(t, err) } func TestDivZero(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x111111111 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x111111111 int 0 / pop -int 1`) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(program, defaultEvalParams(&sb, nil)) - if err != nil { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.NoError(t, err) - require.True(t, cost < 1000) - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +int 1`, v) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + if err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, cost < 1000) + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestModZero(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x111111111 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x111111111 int 0 % pop -int 1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestErr(t *testing.T) { t.Parallel() - program, err := AssembleString(`err -int 1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`err +int 1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + }) } - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) } func TestModSubMulOk(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 35 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 35 int 16 % int 1 @@ -564,152 +769,201 @@ int 1 int 2 * int 4 -==`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestPop(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 int 0 -pop`) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(program, defaultEvalParams(&sb, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb = strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +pop`, v) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb = strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestStackLeftover(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 -int 1`) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(program, defaultEvalParams(&sb, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb = strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 +int 1`, v) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb = strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } func TestStackBytesLeftover(t *testing.T) { t.Parallel() - program, err := AssembleString(`byte 0x10101010`) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(program, defaultEvalParams(&sb, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb = strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`byte 0x10101010`, v) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb = strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } func TestStackEmpty(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 int 1 pop -pop`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +pop`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } func TestArgTooFar(t *testing.T) { t.Parallel() - program, err := AssembleString(`arg_1 -btoi`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = nil - pass, err := Eval(program, defaultEvalParams(&sb, &txn)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`arg_1 +btoi`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) // TODO: Check should know the type stack was wrong + require.True(t, cost < 1000) + sb := strings.Builder{} + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = nil + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } func TestIntcTooFar(t *testing.T) { t.Parallel() - program, err := AssembleString(`intc_1`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = nil - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn}) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`intc_1`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) // TODO: Check should know the type stack was wrong + require.True(t, cost < 1000) + sb := strings.Builder{} + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = nil + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) + } +} + +func TestBytecTooFar(t *testing.T) { + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`bytec_1 +btoi`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) // TODO: Check should know the type stack was wrong + require.True(t, cost < 1000) + sb := strings.Builder{} + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = nil + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } -func TestBytecTooFar(t *testing.T) { +func TestTxnBadField(t *testing.T) { t.Parallel() - program, err := AssembleString(`bytec_1 -btoi`) - require.NoError(t, err) + program := []byte{0x01, 0x31, 0x7f} cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) + require.NoError(t, err) // TODO: Check should know the type stack was wrong require.True(t, cost < 1000) sb := strings.Builder{} var txn transactions.SignedTxn @@ -723,34 +977,30 @@ btoi`) require.Error(t, err) require.False(t, pass) isNotPanic(t, err) -} -func TestTxnBadField(t *testing.T) { - t.Parallel() - program := []byte{0x01, 0x31, 0x7f} - cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = nil - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn}) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + // test txn does not accept ApplicationArgs and Accounts + txnOpcode := opsByName[LogicVersion]["txn"].Opcode + txnaOpcode := opsByName[LogicVersion]["txna"].Opcode + + fields := []TxnField{ApplicationArgs, Accounts} + for _, field := range fields { + source := fmt.Sprintf("txn %s 0", field.String()) + program, err = AssembleString(source) + require.NoError(t, err) + require.Equal(t, txnaOpcode, program[1]) + program[1] = txnOpcode + pass, err = Eval(program, defaultEvalParams(&sb, &txn)) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("invalid txn field %d", field)) + require.False(t, pass) } - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) } func TestGtxnBadIndex(t *testing.T) { t.Parallel() program := []byte{0x01, 0x33, 0x1, 0x01} cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) + require.NoError(t, err) // TODO: Check should know the type stack was wrong require.True(t, cost < 1000) sb := strings.Builder{} var txn transactions.SignedTxn @@ -758,8 +1008,9 @@ func TestGtxnBadIndex(t *testing.T) { txn.Lsig.Args = nil txgroup := make([]transactions.SignedTxn, 1) txgroup[0] = txn - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn, TxnGroup: txgroup}) + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + pass, err := Eval(program, ep) if pass { t.Log(hex.EncodeToString(program)) t.Log(sb.String()) @@ -773,7 +1024,7 @@ func TestGtxnBadField(t *testing.T) { t.Parallel() program := []byte{0x01, 0x33, 0x0, 0x7f} cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) + require.NoError(t, err) // TODO: Check should know the type stack was wrong require.True(t, cost < 1000) sb := strings.Builder{} var txn transactions.SignedTxn @@ -781,8 +1032,9 @@ func TestGtxnBadField(t *testing.T) { txn.Lsig.Args = nil txgroup := make([]transactions.SignedTxn, 1) txgroup[0] = txn - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn, TxnGroup: txgroup}) + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + pass, err := Eval(program, ep) if pass { t.Log(hex.EncodeToString(program)) t.Log(sb.String()) @@ -790,14 +1042,31 @@ func TestGtxnBadField(t *testing.T) { require.Error(t, err) require.False(t, pass) isNotPanic(t, err) + + // test gtxn does not accept ApplicationArgs and Accounts + txnOpcode := opsByName[LogicVersion]["txn"].Opcode + txnaOpcode := opsByName[LogicVersion]["txna"].Opcode + + fields := []TxnField{ApplicationArgs, Accounts} + for _, field := range fields { + source := fmt.Sprintf("txn %s 0", field.String()) + program, err = AssembleString(source) + require.NoError(t, err) + require.Equal(t, txnaOpcode, program[1]) + program[1] = txnOpcode + pass, err = Eval(program, defaultEvalParams(&sb, &txn)) + require.Error(t, err) + require.Contains(t, err.Error(), fmt.Sprintf("invalid txn field %d", field)) + require.False(t, pass) + } } func TestGlobalBadField(t *testing.T) { t.Parallel() program := []byte{0x01, 0x32, 0x7f} cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) require.True(t, cost < 1000) + require.NoError(t, err) // Check does not validates opcode args sb := strings.Builder{} var txn transactions.SignedTxn txn.Lsig.Logic = program @@ -814,7 +1083,9 @@ func TestGlobalBadField(t *testing.T) { func TestArg(t *testing.T) { t.Parallel() - program, err := AssembleString(`arg 0 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`arg 0 arg 1 == arg 2 @@ -825,31 +1096,33 @@ arg 4 len int 9 < -&&`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = [][]byte{ - []byte("aoeu"), - []byte("aoeu"), - []byte("aoeu2"), - []byte("aoeu3"), - []byte("aoeu4"), - } - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, &txn)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +&&`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{ + []byte("aoeu"), + []byte("aoeu"), + []byte("aoeu2"), + []byte("aoeu3"), + []byte("aoeu4"), + } + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } -const globalTestProgram = `global MinTxnFee +const globalV1TestProgram = `global MinTxnFee int 123 == global MinBalance @@ -863,112 +1136,198 @@ int 999 global ZeroAddress txn CloseRemainderTo == -//&& -//global TimeStamp -//int 2069 -//== -//&& -//global Round -//int 999999 -//== && global GroupSize int 1 == -&&` +&& +` + +const globalV2TestProgram = `global LogicSigVersion +int 2 +== +&& +global Round +int 0 +> +&& +global LatestTimestamp +int 0 +> +&& +` func TestGlobal(t *testing.T) { t.Parallel() - for _, globalField := range GlobalFieldNames { - if !strings.Contains(globalTestProgram, globalField) { - t.Errorf("TestGlobal missing field %v", globalField) - } - } - program, err := AssembleString(globalTestProgram) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txgroup := make([]transactions.SignedTxn, 1) - txgroup[0] = txn - sb := strings.Builder{} - block := bookkeeping.Block{} - block.BlockHeader.Round = 999999 - block.BlockHeader.TimeStamp = 2069 - proto := config.ConsensusParams{ - MinTxnFee: 123, - MinBalance: 1000000, - MaxTxnLife: 999, - LogicSigVersion: 1, - LogicSigMaxCost: 20000, - } - ep := EvalParams{ - Trace: &sb, - Txn: &txn, - Proto: &proto, - TxnGroup: txgroup, - } - pass, err := Eval(program, ep) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + type desc struct { + lastField GlobalField + program string + eval func([]byte, EvalParams) (bool, error) + check func([]byte, EvalParams) (int, error) + } + tests := map[uint64]desc{ + 0: {GroupSize, globalV1TestProgram, Eval, Check}, + 1: {GroupSize, globalV1TestProgram, Eval, Check}, + 2: { + LatestTimestamp, globalV1TestProgram + globalV2TestProgram, + func(p []byte, ep EvalParams) (bool, error) { + pass, _, err := EvalStateful(p, ep) + return pass, err + }, + func(program []byte, ep EvalParams) (int, error) { return CheckStateful(program, ep) }, + }, + } + ledger := makeTestLedger(nil) + for v := uint64(0); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + last := tests[v].lastField + testProgram := tests[v].program + check := tests[v].check + eval := tests[v].eval + for _, globalField := range GlobalFieldNames[:last] { + if !strings.Contains(testProgram, globalField) { + t.Errorf("TestGlobal missing field %v", globalField) + } + } + program, err := AssembleStringWithVersion(testProgram, v) + require.NoError(t, err) + cost, err := check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txgroup := make([]transactions.SignedTxn, 1) + txgroup[0] = txn + sb := strings.Builder{} + block := bookkeeping.Block{} + block.BlockHeader.Round = 999999 + block.BlockHeader.TimeStamp = 2069 + proto := config.ConsensusParams{ + MinTxnFee: 123, + MinBalance: 1000000, + MaxTxnLife: 999, + LogicSigVersion: LogicVersion, + LogicSigMaxCost: 20000, + } + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + ep.Proto = &proto + ep.Ledger = ledger + pass, err := eval(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestTypeEnum(t *testing.T) { t.Parallel() - ttypes := []protocol.TxType{ - protocol.PaymentTx, - protocol.KeyRegistrationTx, - protocol.AssetConfigTx, - protocol.AssetTransferTx, - protocol.AssetFreezeTx, - } - // this is explicitly a local copy of the list so that someone - // doesn't accidentally disconnect the doc.go - // typeEnumDescriptions from its need in assembler.go - typeNames := []string{ - "Payment", - "KeyRegistration", - "AssetConfig", - "AssetTransfer", - "AssetFreeze", - } - for i, tt := range ttypes { - symbol := typeNames[i] - t.Run(string(symbol), func(t *testing.T) { - text := fmt.Sprintf(`txn TypeEnum + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + ttypes := []protocol.TxType{ + protocol.PaymentTx, + protocol.KeyRegistrationTx, + protocol.AssetConfigTx, + protocol.AssetTransferTx, + protocol.AssetFreezeTx, + protocol.ApplicationCallTx, + } + // this is explicitly a local copy of the list so that someone + // doesn't accidentally disconnect the doc.go + // typeEnumDescriptions from its need in assembler.go + typeNames := []string{ + "Payment", + "KeyRegistration", + "AssetConfig", + "AssetTransfer", + "AssetFreeze", + "ApplicationCall", + } + for i, tt := range ttypes { + symbol := typeNames[i] + t.Run(string(symbol), func(t *testing.T) { + text := fmt.Sprintf(`txn TypeEnum int %s == txn TypeEnum int %s == &&`, symbol, string(tt)) - program, err := AssembleString(text) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - var txn transactions.SignedTxn - txn.Txn.Type = tt - sb := strings.Builder{} - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn, GroupIndex: 3}) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + program, err := AssembleStringWithVersion(text, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + var txn transactions.SignedTxn + txn.Txn.Type = tt + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.GroupIndex = 3 + pass, err := Eval(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } + }) + } +} + +func TestOnCompletionConstants(t *testing.T) { + t.Parallel() + + // ensure all the OnCompetion values are in OnCompletionValues list + var max int = 100 + var last int = max + for i := 0; i < max; i++ { + oc := transactions.OnCompletion(i) + unknownStringer := "OnCompletion(" + strconv.FormatInt(int64(i), 10) + ")" + if oc.String() == unknownStringer { + last = i + break + } + } + require.Less(t, last, max, "too many OnCompletion constants, adjust max limit") + require.Equal(t, int(invalidOnCompletionConst), last) + require.Equal(t, len(onCompletionConstToUint64), len(onCompletionDescriptions)) + require.Equal(t, len(OnCompletionNames), last) + for v := NoOp; v < invalidOnCompletionConst; v++ { + require.Equal(t, v.String(), OnCompletionNames[int(v)]) + } + + // check constants matching to their values + ep := defaultEvalParams(nil, nil) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + for i := 0; i < last; i++ { + oc := OnCompletionConstType(i) + symbol := oc.String() + require.Contains(t, onCompletionConstToUint64, symbol) + require.Equal(t, uint64(i), onCompletionConstToUint64[symbol]) + t.Run(symbol, func(t *testing.T) { + text := fmt.Sprintf(`int %s +int %s +== +`, symbol, oc) + program, err := AssembleStringWithVersion(text, v) + require.NoError(t, err) + pass, err := Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) }) } } -const testTxnProgramText = `txn Sender +const testTxnProgramTextV1 = `txn Sender arg 0 == txn Receiver @@ -1058,22 +1417,50 @@ arg 7 txn Lease arg 8 == -&&` +&& +` -func TestTxn(t *testing.T) { - t.Parallel() - for _, txnField := range TxnFieldNames { - if !strings.Contains(testTxnProgramText, txnField) { - if txnField != FirstValidTime.String() { - t.Errorf("TestTxn missing field %v", txnField) - } - } - } - program, err := AssembleString(testTxnProgramText) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) +var testTxnProgramText = testTxnProgramTextV1 + `txn ApplicationID +int 123 +== +&& +txn OnCompletion +int 0 +== +&& +txna ApplicationArgs 0 +byte 0x706179 +== +&& +txn NumAppArgs +int 1 +== +&& +txna Accounts 0 +arg 0 +== +&& +txn NumAccounts +int 1 +== +&& +byte b64 UHJvZ3JhbQ== // Program +txn ApprovalProgram +concat +sha512_256 +arg 9 +== +&& +byte b64 UHJvZ3JhbQ== // Program +txn ClearStateProgram +concat +sha512_256 +arg 10 +== +&& +` + +func makeSampleTxn() transactions.SignedTxn { var txn transactions.SignedTxn copy(txn.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) copy(txn.Txn.Receiver[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui01")) @@ -1096,23 +1483,150 @@ func TestTxn(t *testing.T) { txn.Txn.AssetSender = txn.Txn.Receiver txn.Txn.AssetReceiver = txn.Txn.CloseRemainderTo txn.Txn.AssetCloseTo = txn.Txn.Sender + txn.Txn.ApplicationID = basics.AppIndex(123) + txn.Txn.Accounts = make([]basics.Address, 1) + txn.Txn.Accounts[0] = txn.Txn.Receiver + txn.Txn.ApplicationArgs = make([][]byte, 1) + txn.Txn.ApplicationArgs[0] = []byte(protocol.PaymentTx) + return txn +} + +func makeSampleTxnGroup(txn transactions.SignedTxn) []transactions.SignedTxn { + txgroup := make([]transactions.SignedTxn, 2) + txgroup[0] = txn + txgroup[1].Txn.Amount.Raw = 42 + txgroup[1].Txn.Fee.Raw = 1066 + txgroup[1].Txn.FirstValid = 42 + txgroup[1].Txn.LastValid = 1066 + txgroup[1].Txn.Sender = txn.Txn.Receiver + txgroup[1].Txn.Receiver = txn.Txn.Sender + return txgroup +} + +func TestTxn(t *testing.T) { + t.Parallel() + for _, txnField := range TxnFieldNames { + if !strings.Contains(testTxnProgramText, txnField) { + if txnField != FirstValidTime.String() { + t.Errorf("TestTxn missing field %v", txnField) + } + } + } + + tests := map[uint64]string{ + 1: testTxnProgramTextV1, + 2: testTxnProgramText, + } + + clearProgram, err := AssembleStringWithVersion("int 1", 1) + require.NoError(t, err) + + for v, source := range tests { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + txn := makeSampleTxn() + txn.Txn.ApprovalProgram = program + txn.Txn.ClearStateProgram = clearProgram + txn.Lsig.Logic = program + txid := txn.Txn.ID() + programHash := HashProgram(program) + clearProgramHash := HashProgram(clearProgram) + txn.Lsig.Args = [][]byte{ + txn.Txn.Sender[:], + txn.Txn.Receiver[:], + txn.Txn.CloseRemainderTo[:], + txn.Txn.VotePK[:], + txn.Txn.SelectionPK[:], + txn.Txn.Note, + {0, 0, 0, 0, 0, 0, 0, 1}, + txid[:], + txn.Txn.Lease[:], + programHash[:], + clearProgramHash[:], + } + sb := strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.GroupIndex = 3 + pass, err := Eval(program, ep) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestCachedTxIDs(t *testing.T) { + t.Parallel() + cachedTxnProg := ` +gtxn 0 TxID +arg 0 +== +bz fail + +gtxn 0 TxID +arg 0 +== +bz fail + +txn TxID +arg 0 +== +bz fail + +txn TxID +arg 0 +== +bz fail + +gtxn 1 TxID +arg 1 +== +bz fail + +gtxn 1 TxID +arg 1 +== +bz fail + +success: +int 1 +return + +fail: +int 0 +return +` + program, err := AssembleStringWithVersion(cachedTxnProg, 2) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + if err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, cost < 1000) + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) txn.Lsig.Logic = program - txid := txn.Txn.ID() + txid0 := txgroup[0].ID() + txid1 := txgroup[1].ID() txn.Lsig.Args = [][]byte{ - txn.Txn.Sender[:], - txn.Txn.Receiver[:], - txn.Txn.CloseRemainderTo[:], - txn.Txn.VotePK[:], - txn.Txn.SelectionPK[:], - txn.Txn.Note, - []byte{0, 0, 0, 0, 0, 0, 0, 1}, - txid[:], - txn.Txn.Lease[:], + txid0[:], + txid1[:], } - sb := strings.Builder{} - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn, GroupIndex: 3}) - if !pass { + sb = strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + pass, err := Eval(program, ep) + if !pass || err != nil { t.Log(hex.EncodeToString(program)) t.Log(sb.String()) } @@ -1122,7 +1636,7 @@ func TestTxn(t *testing.T) { func TestGtxn(t *testing.T) { t.Parallel() - program, err := AssembleString(`gtxn 1 Amount + gtxnTextV1 := `gtxn 1 Amount int 42 == gtxn 1 Fee @@ -1172,61 +1686,250 @@ int 1 global GroupSize int 2 == -&&`) - require.NoError(t, err) - sb := strings.Builder{} - cost, err := Check(program, defaultEvalParams(&sb, nil)) - if err != nil { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +&& +` + + gtxnText := gtxnTextV1 + `gtxna 0 ApplicationArgs 0 +byte 0x706179 +== +&& +gtxn 0 NumAppArgs +int 1 +== +&& +gtxna 0 Accounts 0 +gtxn 0 Sender +== +&& +gtxn 0 NumAccounts +int 1 +== +&& +` + + tests := map[uint64]string{ + 1: gtxnTextV1, + 2: gtxnText, + } + + for v, source := range tests { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + sb := strings.Builder{} + cost, err := Check(program, defaultEvalParams(&sb, nil)) + if err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, cost < 1000) + txn := makeSampleTxn() + txgroup := makeSampleTxnGroup(txn) + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{ + txn.Txn.Sender[:], + txn.Txn.Receiver[:], + txn.Txn.CloseRemainderTo[:], + txn.Txn.VotePK[:], + txn.Txn.SelectionPK[:], + txn.Txn.Note, + } + sb = strings.Builder{} + ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = txgroup + pass, err := Eval(program, ep) + if !pass || err != nil { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } +} + +func TestTxna(t *testing.T) { + t.Parallel() + source := `txna Accounts 1 +txna ApplicationArgs 0 +== +` + program, err := AssembleString(source) require.NoError(t, err) - require.True(t, cost < 1000) - txgroup := make([]transactions.SignedTxn, 2) var txn transactions.SignedTxn - copy(txn.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) - copy(txn.Txn.Receiver[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui01")) - copy(txn.Txn.CloseRemainderTo[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui02")) - copy(txn.Txn.VotePK[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui03")) - copy(txn.Txn.SelectionPK[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui04")) - txn.Txn.Note = []byte("fnord") - txn.Txn.Fee.Raw = 1337 - txn.Txn.FirstValid = 42 - txn.Txn.LastValid = 1066 - txn.Txn.Amount.Raw = 1000000 - txn.Txn.VoteFirst = 1317 - txn.Txn.VoteLast = 17776 - txn.Txn.VoteKeyDilution = 1 + txn.Txn.Accounts = make([]basics.Address, 1) + txn.Txn.Accounts[0] = txn.Txn.Sender + txn.Txn.ApplicationArgs = make([][]byte, 1) + txn.Txn.ApplicationArgs[0] = []byte(protocol.PaymentTx) + txgroup := make([]transactions.SignedTxn, 1) txgroup[0] = txn - txgroup[1].Txn.Amount.Raw = 42 - txgroup[1].Txn.Fee.Raw = 1066 - txgroup[1].Txn.FirstValid = 42 - txgroup[1].Txn.LastValid = 1066 - txgroup[1].Txn.Sender = txn.Txn.Receiver - txgroup[1].Txn.Receiver = txn.Txn.Sender - txn.Lsig.Logic = program - txn.Lsig.Args = [][]byte{ - txn.Txn.Sender[:], - txn.Txn.Receiver[:], - txn.Txn.CloseRemainderTo[:], - txn.Txn.VotePK[:], - txn.Txn.SelectionPK[:], - txn.Txn.Note, - } - sb = strings.Builder{} - proto := defaultEvalProto() - pass, err := Eval(program, EvalParams{Proto: &proto, Trace: &sb, Txn: &txn, TxnGroup: txgroup}) - if !pass || err != nil { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } + ep := defaultEvalParams(nil, &txn) + ep.TxnGroup = txgroup + _, err = Eval(program, ep) + require.NoError(t, err) + + // modify txn field + saved := program[2] + program[2] = 0x01 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "txna unsupported field") + + // modify txn field to unknown one + program[2] = 99 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field 99") + + // modify txn array index + program[2] = saved + saved = program[3] + program[3] = 0x02 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid Accounts index") + + // modify txn array index in the second opcode + program[3] = saved + saved = program[6] + program[6] = 0x01 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid ApplicationArgs index") + program[6] = saved + + // check special case: Account 0 == Sender + // even without any additional context + source = `txna Accounts 0 +txn Sender +== +` + program2, err := AssembleString(source) + require.NoError(t, err) + var txn2 transactions.SignedTxn + copy(txn2.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) + ep2 := defaultEvalParams(nil, &txn2) + pass, err := Eval(program2, ep2) + require.NoError(t, err) + require.True(t, pass) + + // check gtxna + source = `gtxna 0 Accounts 1 +txna ApplicationArgs 0 +==` + program, err = AssembleString(source) + require.NoError(t, err) + _, err = Eval(program, ep) + require.NoError(t, err) + + // modify gtxn index + saved = program[2] + program[2] = 0x01 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna lookup TxnGroup[1] but it only has 1") + + // modify gtxn field + program[2] = saved + saved = program[3] + program[3] = 0x01 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "gtxna unsupported field") + + // modify gtxn field to unknown one + program[3] = 99 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid txn field 99") + + // modify gtxn array index + program[3] = saved + saved = program[4] + program[4] = 0x02 + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid Accounts index") + program[4] = saved + + // check special case: Account 0 == Sender + // even without any additional context + source = `gtxna 0 Accounts 0 +txn Sender +== +` + program3, err := AssembleString(source) + require.NoError(t, err) + var txn3 transactions.SignedTxn + copy(txn2.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) + txgroup3 := make([]transactions.SignedTxn, 1) + txgroup3[0] = txn3 + ep3 := defaultEvalParams(nil, &txn3) + ep3.TxnGroup = txgroup3 + pass, err = Eval(program3, ep3) + require.NoError(t, err) + require.True(t, pass) +} + +// check empty values in ApplicationArgs and Account +func TestTxnaEmptyValues(t *testing.T) { + t.Parallel() + source := `txna ApplicationArgs 0 +btoi +int 0 +== +` + program, err := AssembleString(source) + require.NoError(t, err) + + var txn transactions.SignedTxn + txn.Txn.ApplicationArgs = make([][]byte, 1) + txn.Txn.ApplicationArgs[0] = []byte("") + txgroup := make([]transactions.SignedTxn, 1) + txgroup[0] = txn + ep := defaultEvalParams(nil, &txn) + ep.TxnGroup = txgroup + pass, err := Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + txn.Txn.ApplicationArgs[0] = nil + txgroup[0] = txn + ep.TxnGroup = txgroup + pass, err = Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + + source2 := `txna Accounts 1 +global ZeroAddress +== +` + program2, err := AssembleString(source2) + require.NoError(t, err) + + var txn2 transactions.SignedTxn + txn2.Txn.Accounts = make([]basics.Address, 1) + txn2.Txn.Accounts[0] = basics.Address{} + txgroup2 := make([]transactions.SignedTxn, 1) + txgroup2[0] = txn2 + ep2 := defaultEvalParams(nil, &txn2) + ep2.TxnGroup = txgroup2 + pass, err = Eval(program2, ep2) + require.NoError(t, err) + require.True(t, pass) + txn2.Txn.Accounts = make([]basics.Address, 1) + txgroup2[0] = txn + ep2.TxnGroup = txgroup2 + pass, err = Eval(program2, ep2) require.NoError(t, err) require.True(t, pass) } func TestBitOps(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 0x17 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0x17 int 0x3e & // == 0x16 int 0x0a @@ -1237,7 +1940,44 @@ int 0x0f int 0x300 | int 0x310 -==`) +==`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) + } +} + +func TestStringOps(t *testing.T) { + t.Parallel() + program, err := assembleStringWithTrace(t, `byte 0x123456789abc +substring 1 3 +byte 0x3456 +== +byte 0x12 +byte 0x3456 +byte 0x789abc +concat +concat +byte 0x123456789abc +== +&& +byte 0x123456789abc +int 1 +int 3 +substring3 +byte 0x3456 +== +&&`, 2) require.NoError(t, err) cost, err := Check(program, defaultEvalParams(nil, nil)) require.NoError(t, err) @@ -1252,9 +1992,131 @@ int 0x310 require.True(t, pass) } +func TestConsOverflow(t *testing.T) { + t.Parallel() + program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +dup +concat +len`, 2) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) +} + +func TestSubstringFlop(t *testing.T) { + t.Parallel() + // fails in compiler + program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 +substring 4 2 +len`, 2) + require.Error(t, err) + require.Nil(t, program) + + // fails at runtime + program, err = assembleStringWithTrace(t, `byte 0xf000000000000000 +int 4 +int 2 +substring3 +len`, 2) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) + + // fails at runtime + program, err = assembleStringWithTrace(t, `byte 0xf000000000000000 +int 4 +int 0xFFFFFFFFFFFFFFFE +substring3 +len`, 2) + require.NoError(t, err) + cost, err = Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + pass, err = Eval(program, defaultEvalParams(nil, nil)) + require.False(t, pass) + require.Error(t, err) + require.Contains(t, err.Error(), "substring range beyond length of string") +} + +func TestSubstringRange(t *testing.T) { + t.Parallel() + program, err := assembleStringWithTrace(t, `byte 0xf000000000000000 +substring 2 99 +len`, 2) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) +} + func TestLoadStore(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 37 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 37 int 37 store 1 byte 0xabbacafe @@ -1268,25 +2130,27 @@ byte 0xabbacafe load 0 load 1 + -&&`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +&&`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } -func assembleStringWithTrace(t testing.TB, text string) ([]byte, error) { +func assembleStringWithTrace(t testing.TB, text string, version uint64) ([]byte, error) { sr := strings.NewReader(text) sb := strings.Builder{} - ops := OpStream{Trace: &sb} + ops := OpStream{Trace: &sb, Version: version} err := ops.Assemble(sr) if err != nil { t.Log(sb.String()) @@ -1297,7 +2161,7 @@ func assembleStringWithTrace(t testing.TB, text string) ([]byte, error) { func TestLoadStore2(t *testing.T) { t.Parallel() - program, err := assembleStringWithTrace(t, `int 2 + progText := `int 2 int 3 byte 0xaa store 44 @@ -1307,19 +2171,24 @@ load 43 load 42 + int 5 -==`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==` + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := assembleStringWithTrace(t, progText, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } const testCompareProgramText = `int 35 @@ -1384,19 +2253,23 @@ byte 0xf00d func TestCompares(t *testing.T) { t.Parallel() - program, err := AssembleString(testCompareProgramText) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(nil, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(testCompareProgramText, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(nil, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestKeccak256(t *testing.T) { @@ -1407,22 +2280,27 @@ func TestKeccak256(t *testing.T) { blob=b'fnord' sha3.keccak_256(blob).hexdigest() */ - program, err := AssembleString(`byte 0x666E6F7264 + progText := `byte 0x666E6F7264 keccak256 byte 0xc195eca25a6f4c82bfba0287082ddb0d602ae9230f9cf1f1a40b68f8e2c41567 -==`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(nil, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==` + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(progText, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(nil, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func TestSHA512_256(t *testing.T) { @@ -1436,23 +2314,28 @@ func TestSHA512_256(t *testing.T) { digest.update(b'fnord') base64.b16encode(digest.finalize()) */ - program, err := AssembleString(`byte 0x666E6F7264 + progText := `byte 0x666E6F7264 sha512_256 byte 0x98D2C31612EA500279B6753E5F6E780CA63EBA8274049664DAD66A2565ED1D2A -==`) - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) +==` + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(progText, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.NoError(t, err) + require.True(t, pass) + }) } - require.NoError(t, err) - require.True(t, pass) } func isNotPanic(t *testing.T, err error) { @@ -1466,151 +2349,155 @@ func isNotPanic(t *testing.T, err error) { func TestStackUnderflow(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1`) - program = append(program, 0x08) // + - require.NoError(t, err) - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + program = append(program, 0x08) // + + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestWrongStackTypeRuntime(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1`) - require.NoError(t, err) - program = append(program, 0x01, 0x15) // sha256, len - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + program = append(program, 0x01, 0x15) // sha256, len + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestEqMismatch(t *testing.T) { t.Parallel() - program, err := AssembleString(`byte 0x1234 -int 1`) - require.NoError(t, err) - program = append(program, 0x12) // == - cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) // Check should know the type stack was wrong - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`byte 0x1234 +int 1`, v) + require.NoError(t, err) + program = append(program, 0x12) // == + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) // TODO: Check should know the type stack was wrong + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestNeqMismatch(t *testing.T) { t.Parallel() - program, err := AssembleString(`byte 0x1234 -int 1`) - require.NoError(t, err) - program = append(program, 0x13) // != - cost, err := Check(program, defaultEvalParams(nil, nil)) - //require.Error(t, err) // Check should know the type stack was wrong - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`byte 0x1234 +int 1`, v) + require.NoError(t, err) + program = append(program, 0x13) // != + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) // TODO: Check should know the type stack was wrong + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestWrongStackTypeRuntime2(t *testing.T) { t.Parallel() - program, err := AssembleString(`byte 0x1234 -int 1`) - require.NoError(t, err) - program = append(program, 0x08) // + - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.NoError(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, _ := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`byte 0x1234 +int 1`, v) + require.NoError(t, err) + program = append(program, 0x08) // + + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, _ := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } func TestIllegalOp(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1`) - require.NoError(t, err) - for opcode, spec := range opsByOpcode { - if spec.op == nil { - program = append(program, byte(opcode)) - break - } - } - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.False(t, pass) - isNotPanic(t, err) -} - -func TestShortProgram(t *testing.T) { - t.Parallel() - program, err := AssembleString(`int 1 -bnz done -done:`) - require.NoError(t, err) - program = program[:len(program)-1] - cost, err := Check(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.True(t, cost < 1000) - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, nil)) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + for opcode, spec := range opsByOpcode[v] { + if spec.op == nil { + program = append(program, byte(opcode)) + break + } + } + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) } - require.False(t, pass) - isNotPanic(t, err) } -func TestShortBytecblock(t *testing.T) { +func TestShortProgram(t *testing.T) { t.Parallel() - fullprogram, err := AssembleString(`bytecblock 0x123456 0xababcdcd`) - require.NoError(t, err) - fullprogram[2] = 50 // fake 50 elements - for i := 2; i < len(fullprogram); i++ { - program := fullprogram[:i] - t.Run(hex.EncodeToString(program), func(t *testing.T) { + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 +bnz done +done:`, v) + require.NoError(t, err) + program = program[:len(program)-1] cost, err := Check(program, defaultEvalParams(nil, nil)) require.Error(t, err) - isNotPanic(t, err) require.True(t, cost < 1000) sb := strings.Builder{} pass, err := Eval(program, defaultEvalParams(&sb, nil)) @@ -1624,9 +2511,54 @@ func TestShortBytecblock(t *testing.T) { } } +func TestShortProgramTrue(t *testing.T) { + t.Parallel() + program, err := assembleStringWithTrace(t, `intcblock 1 +intc 0 +intc 0 +bnz done +done:`, 2) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + require.NoError(t, err) + require.True(t, pass) +} +func TestShortBytecblock(t *testing.T) { + t.Parallel() + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + fullprogram, err := AssembleStringWithVersion(`bytecblock 0x123456 0xababcdcd "test"`, v) + require.NoError(t, err) + fullprogram[2] = 50 // fake 50 elements + for i := 2; i < len(fullprogram); i++ { + program := fullprogram[:i] + t.Run(hex.EncodeToString(program), func(t *testing.T) { + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + isNotPanic(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + isNotPanic(t, err) + }) + } + }) + } +} + func TestShortBytecblock2(t *testing.T) { t.Parallel() sources := []string{ + "02260180fe83f88fe0bf80ff01aa", "01260180fe83f88fe0bf80ff01aa", "0026efbfbdefbfbd02", "0026efbfbdefbfbd30", @@ -1662,54 +2594,56 @@ func checkPanic(cx *evalContext) int { func TestPanic(t *testing.T) { log := logging.TestingLog(t) - program, err := AssembleString(`int 1`) - require.NoError(t, err) - var hackedOpcode int - var oldSpec OpSpec - var oldOz opSize - for opcode, spec := range opsByOpcode { - if spec.op == nil { - hackedOpcode = opcode - oldSpec = spec - opsByOpcode[opcode].op = opPanic - program = append(program, byte(opcode)) - oldOz = opSizeByOpcode[opcode] - opSizeByOpcode[opcode].checkFunc = checkPanic - break - } - } - sb := strings.Builder{} - params := defaultEvalParams(&sb, nil) - params.Logger = log - _, err = Check(program, params) - require.Error(t, err) - if pe, ok := err.(PanicError); ok { - require.Equal(t, panicString, pe.PanicValue) - pes := pe.Error() - require.True(t, strings.Contains(pes, "panic")) - } else { - t.Errorf("expected PanicError object but got %T %#v", err, err) - } - sb = strings.Builder{} - var txn transactions.SignedTxn - txn.Lsig.Logic = program - params = defaultEvalParams(&sb, &txn) - params.Logger = log - pass, err := Eval(program, params) - if pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.False(t, pass) - if pe, ok := err.(PanicError); ok { - require.Equal(t, panicString, pe.PanicValue) - pes := pe.Error() - require.True(t, strings.Contains(pes, "panic")) - } else { - t.Errorf("expected PanicError object but got %T %#v", err, err) + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + var hackedOpcode int + var oldSpec OpSpec + for opcode, spec := range opsByOpcode[v] { + if spec.op == nil { + hackedOpcode = opcode + oldSpec = spec + opsByOpcode[v][opcode].op = opPanic + opsByOpcode[v][opcode].Modes = modeAny + opsByOpcode[v][opcode].opSize.checkFunc = checkPanic + program = append(program, byte(opcode)) + break + } + } + sb := strings.Builder{} + params := defaultEvalParams(&sb, nil) + params.Logger = log + _, err = Check(program, params) + require.Error(t, err) + if pe, ok := err.(PanicError); ok { + require.Equal(t, panicString, pe.PanicValue) + pes := pe.Error() + require.True(t, strings.Contains(pes, "panic")) + } else { + t.Errorf("expected PanicError object but got %T %#v", err, err) + } + sb = strings.Builder{} + var txn transactions.SignedTxn + txn.Lsig.Logic = program + params = defaultEvalParams(&sb, &txn) + params.Logger = log + pass, err := Eval(program, params) + if pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.False(t, pass) + if pe, ok := err.(PanicError); ok { + require.Equal(t, panicString, pe.PanicValue) + pes := pe.Error() + require.True(t, strings.Contains(pes, "panic")) + } else { + t.Errorf("expected PanicError object but got %T %#v", err, err) + } + opsByOpcode[v][hackedOpcode] = oldSpec + }) } - opsByOpcode[hackedOpcode] = oldSpec - opSizeByOpcode[hackedOpcode] = oldOz } func TestProgramTooNew(t *testing.T) { @@ -1745,9 +2679,12 @@ func TestProgramProtoForbidden(t *testing.T) { proto := config.ConsensusParams{ LogicSigVersion: EvalMaxVersion - 1, } - _, err := Check(program[:vlen], EvalParams{Proto: &proto}) + ep := EvalParams{} + ep.Proto = &proto + _, err := Check(program[:vlen], ep) require.Error(t, err) - pass, err := Eval(program[:vlen], EvalParams{Proto: &proto}) + ep.Txn = &transactions.SignedTxn{} + pass, err := Eval(program[:vlen], ep) require.Error(t, err) require.False(t, pass) isNotPanic(t, err) @@ -1755,68 +2692,110 @@ func TestProgramProtoForbidden(t *testing.T) { func TestMisalignedBranch(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 bnz done bytecblock 0x01234576 0xababcdcd 0xf000baad done: -int 1`) - require.NoError(t, err) - //t.Log(hex.EncodeToString(program)) - canonicalProgramBytes, err := hex.DecodeString("01200101224000112603040123457604ababcdcd04f000baad22") - require.NoError(t, err) - require.Equal(t, program, canonicalProgramBytes) - program[7] = 3 // clobber the branch offset to be in the middle of the bytecblock - _, err = Check(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "aligned")) - pass, err := Eval(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) +int 1`, v) + require.NoError(t, err) + //t.Log(hex.EncodeToString(program)) + canonicalProgramString := mutateProgVersion(v, "01200101224000112603040123457604ababcdcd04f000baad22") + canonicalProgramBytes, err := hex.DecodeString(canonicalProgramString) + require.NoError(t, err) + require.Equal(t, program, canonicalProgramBytes) + program[7] = 3 // clobber the branch offset to be in the middle of the bytecblock + _, err = Check(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "aligned")) + pass, err := Eval(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) + } } func TestBranchTooFar(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 bnz done bytecblock 0x01234576 0xababcdcd 0xf000baad done: -int 1`) - require.NoError(t, err) - //t.Log(hex.EncodeToString(program)) - canonicalProgramBytes, err := hex.DecodeString("01200101224000112603040123457604ababcdcd04f000baad22") - require.NoError(t, err) - require.Equal(t, program, canonicalProgramBytes) - program[7] = 200 // clobber the branch offset to be beyond the end of the program - _, err = Check(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "beyond end of program")) - pass, err := Eval(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) +int 1`, v) + require.NoError(t, err) + //t.Log(hex.EncodeToString(program)) + canonicalProgramString := mutateProgVersion(v, "01200101224000112603040123457604ababcdcd04f000baad22") + canonicalProgramBytes, err := hex.DecodeString(canonicalProgramString) + require.NoError(t, err) + require.Equal(t, program, canonicalProgramBytes) + program[7] = 200 // clobber the branch offset to be beyond the end of the program + _, err = Check(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), "beyond end of program")) + pass, err := Eval(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.False(t, pass) + isNotPanic(t, err) + }) + } } func TestBranchTooLarge(t *testing.T) { t.Parallel() - program, err := AssembleString(`int 1 + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1 bnz done bytecblock 0x01234576 0xababcdcd 0xf000baad done: -int 1`) - require.NoError(t, err) - //t.Log(hex.EncodeToString(program)) - canonicalProgramBytes, err := hex.DecodeString("01200101224000112603040123457604ababcdcd04f000baad22") - require.NoError(t, err) - require.Equal(t, program, canonicalProgramBytes) - program[6] = 0xff // clobber the branch offset - _, err = Check(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.True(t, strings.Contains(err.Error(), "too large")) - pass, err := Eval(program, defaultEvalParams(nil, nil)) - require.Error(t, err) - require.False(t, pass) - isNotPanic(t, err) +int 1`, v) + require.NoError(t, err) + //t.Log(hex.EncodeToString(program)) + canonicalProgramString := mutateProgVersion(v, "01200101224000112603040123457604ababcdcd04f000baad22") + canonicalProgramBytes, err := hex.DecodeString(canonicalProgramString) + require.NoError(t, err) + require.Equal(t, program, canonicalProgramBytes) + program[6] = 0xff // clobber the branch offset + _, err = Check(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "too large") + pass, err := Eval(program, defaultEvalParams(nil, nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "too large") + require.False(t, pass) + isNotPanic(t, err) + }) + } + branches := []string{ + "bz done", + "b done", + } + template := `int 0 +%s +done: +int 1 +` + ep := defaultEvalParams(nil, nil) + for _, line := range branches { + t.Run(fmt.Sprintf("branch=%s", line), func(t *testing.T) { + source := fmt.Sprintf(template, line) + program, err := AssembleString(source) + require.NoError(t, err) + program[7] = 0xff // clobber the branch offset + program[8] = 0xff // clobber the branch offset + _, err = Check(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "too large") + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "too large") + require.False(t, pass) + }) + } } /* @@ -2103,11 +3082,10 @@ func benchmarkBasicProgram(b *testing.B, source string) { require.True(b, cost < 2000) //b.Logf("%d bytes of program", len(program)) //b.Log(hex.EncodeToString(program)) - proto := defaultEvalProto() b.ResetTimer() sb := strings.Builder{} // Trace: &sb for i := 0; i < b.N; i++ { - pass, err := Eval(program, EvalParams{Proto: &proto}) + pass, err := Eval(program, defaultEvalParams(nil, nil)) if !pass { b.Log(sb.String()) } @@ -2148,8 +3126,8 @@ func benchmarkExpensiveProgram(b *testing.B, source string) { func BenchmarkAddx64(b *testing.B) { progs := [][]string{ - []string{"add long stack", addBenchmarkSource}, - []string{"add small stack", addBenchmark2Source}, + {"add long stack", addBenchmarkSource}, + {"add small stack", addBenchmark2Source}, } for _, pp := range progs { b.Run(pp[0], func(b *testing.B) { @@ -2215,44 +3193,49 @@ func TestEd25519verify(t *testing.T) { require.NoError(t, err) pk := basics.Address(c.SignatureVerifier) pkStr := pk.String() - program, err := AssembleString(fmt.Sprintf(`arg 0 + + for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(fmt.Sprintf(`arg 0 arg 1 addr %s -ed25519verify`, pkStr)) - require.NoError(t, err) - sig := c.Sign(Msg{ - ProgramHash: crypto.HashObj(Program(program)), - Data: data[:], - }) - var txn transactions.SignedTxn - txn.Lsig.Logic = program - txn.Lsig.Args = [][]byte{data[:], sig[:]} - sb := strings.Builder{} - pass, err := Eval(program, defaultEvalParams(&sb, &txn)) - if !pass { - t.Log(hex.EncodeToString(program)) - t.Log(sb.String()) - } - require.True(t, pass) - require.NoError(t, err) +ed25519verify`, pkStr), v) + require.NoError(t, err) + sig := c.Sign(Msg{ + ProgramHash: crypto.HashObj(Program(program)), + Data: data[:], + }) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Lsig.Args = [][]byte{data[:], sig[:]} + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, &txn)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) - // short sig will fail - txn.Lsig.Args[1] = sig[1:] - pass, err = Eval(program, defaultEvalParams(nil, &txn)) - require.False(t, pass) - require.Error(t, err) - isNotPanic(t, err) + // short sig will fail + txn.Lsig.Args[1] = sig[1:] + pass, err = Eval(program, defaultEvalParams(nil, &txn)) + require.False(t, pass) + require.Error(t, err) + isNotPanic(t, err) - // flip a bit and it should not pass - msg1 := "52fdfc072182654f163f5f0f9a621d729566c74d0aa413bf009c9800418c19cd" - data1, err := hex.DecodeString(msg1) - require.NoError(t, err) - txn.Lsig.Args = [][]byte{data1, sig[:]} - sb1 := strings.Builder{} - pass1, err := Eval(program, defaultEvalParams(&sb1, &txn)) - require.False(t, pass1) - require.NoError(t, err) - isNotPanic(t, err) + // flip a bit and it should not pass + msg1 := "52fdfc072182654f163f5f0f9a621d729566c74d0aa413bf009c9800418c19cd" + data1, err := hex.DecodeString(msg1) + require.NoError(t, err) + txn.Lsig.Args = [][]byte{data1, sig[:]} + sb1 := strings.Builder{} + pass1, err := Eval(program, defaultEvalParams(&sb1, &txn)) + require.False(t, pass1) + require.NoError(t, err) + isNotPanic(t, err) + }) + } } func BenchmarkEd25519Verifyx1(b *testing.B) { @@ -2287,8 +3270,7 @@ ed25519verify`, pkStr)) txn.Lsig.Logic = programs[i] txn.Lsig.Args = [][]byte{data[i][:], signatures[i][:]} sb := strings.Builder{} - proto := defaultEvalProto() - ep := EvalParams{Proto: &proto, Txn: &txn, Trace: &sb} + ep := defaultEvalParams(&sb, &txn) pass, err := Eval(programs[i], ep) if !pass { b.Log(hex.EncodeToString(programs[i])) @@ -2328,3 +3310,254 @@ func BenchmarkCheckx5(b *testing.B) { } } } + +func TestStackValues(t *testing.T) { + t.Parallel() + + actual := oneInt.plus(oneInt) + require.Equal(t, twoInts, actual) + + actual = oneInt.plus(oneAny) + require.Equal(t, StackTypes{StackUint64, StackAny}, actual) + + actual = twoInts.plus(oneBytes) + require.Equal(t, StackTypes{StackUint64, StackUint64, StackBytes}, actual) + + actual = oneInt.plus(oneBytes).plus(oneAny) + require.Equal(t, StackTypes{StackUint64, StackBytes, StackAny}, actual) +} + +func TestEvalVersions(t *testing.T) { + t.Parallel() + + text := `int 1 +txna ApplicationArgs 0 +pop +` + program, err := AssembleString(text) + require.NoError(t, err) + + ep := defaultEvalParams(nil, nil) + ep.Txn = &transactions.SignedTxn{} + ep.Txn.Txn.ApplicationArgs = [][]byte{[]byte("test")} + _, err = Eval(program, ep) + require.NoError(t, err) + + ep = defaultEvalParamsV1(nil, nil) + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "greater than protocol supported version 1") + + // hack the version and fail on illegal opcode + program[0] = 0x1 + ep = defaultEvalParamsV1(nil, nil) + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "illegal opcode 0x36") // txna +} + +func TestStepErrors(t *testing.T) { + // This test modifies opsByName table, do not run in parallel + + source := `intcblock 0 +intc_0 +intc_0 ++ +` + program, err := AssembleString(source) + require.NoError(t, err) + + ep := defaultEvalParams(nil, nil) + + origSpec := opsByName[LogicVersion]["+"] + spec := origSpec + defer func() { + // restore, opsByOpcode is global + opsByOpcode[LogicVersion][spec.Opcode] = origSpec + }() + + spec.op = func(cx *evalContext) { + // overflow + cx.stack = make([]stackValue, 2000) + } + opsByOpcode[LogicVersion][spec.Opcode] = spec + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "stack overflow") +} + +func TestDup(t *testing.T) { + t.Parallel() + + text := `int 1 +dup +== +bnz dup_ok +err +dup_ok: +int 1 +int 2 +dup2 // expected 1, 2, 1, 2 +int 2 +== +bz error +int 1 +== +bz error +int 2 +== +bz error +int 1 +== +bz error +b exit +error: +err +exit: +int 1 +` + ep := defaultEvalParams(nil, nil) + + program, err := AssembleString(text) + require.NoError(t, err) + pass, err := Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `dup2` + program, err = AssembleString(text) + require.NoError(t, err) + pass, err = Eval(program, ep) + require.Error(t, err) + require.False(t, pass) + + text = `int 1 +dup2 +` + program, err = AssembleString(text) + require.NoError(t, err) + pass, err = Eval(program, ep) + require.Error(t, err) + require.False(t, pass) +} + +func TestStringLiteral(t *testing.T) { + t.Parallel() + + text := `byte "foo bar" +byte b64(Zm9vIGJhcg==) +== +` + ep := defaultEvalParams(nil, nil) + + program, err := AssembleString(text) + require.NoError(t, err) + pass, err := Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `byte "foo bar // not a comment" +byte b64(Zm9vIGJhciAvLyBub3QgYSBjb21tZW50) +== +` + program, err = AssembleString(text) + require.NoError(t, err) + pass, err = Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +func TestArgType(t *testing.T) { + t.Parallel() + + var sv stackValue + require.Equal(t, StackUint64, sv.argType()) + sv.Bytes = []byte("") + require.Equal(t, StackBytes, sv.argType()) + sv.Uint = 1 + require.Equal(t, StackBytes, sv.argType()) + sv.Bytes = nil + require.Equal(t, StackUint64, sv.argType()) +} + +// check all v2 opcodes: allowed in v2 and not allowed in v1 and v0 +func TestAllowedOpcodesV2(t *testing.T) { + t.Parallel() + + tests := map[string]string{ + "txna": "txna Accounts 0", + "gtxna": "gtxna 0 ApplicationArgs 0", + "bz": "bz l\nl:", + "b": "b l\nl:", + "return": "int 1\nreturn", + "dup2": "dup2", + "concat": "byte 0x41\ndup\nconcat", + "substring": "byte 0x41\nsubstring 0 1", + "substring3": "byte 0x41\ndup\ndup\nsubstring3", + "balance": "int 1\nbalance", + "app_opted_in": "int 0\ndup\napp_opted_in", + "app_local_get": "int 0\nbyte 0x41\napp_local_get", + "app_local_get_ex": "int 0\ndup\nbyte 0x41\napp_local_get_ex", + "app_global_get": "int 0\nbyte 0x41\napp_global_get", + "app_global_get_ex": "int 0\nbyte 0x41\napp_global_get_ex", + "app_local_put": "int 0\ndup\nbyte 0x41\napp_local_put", + "app_global_put": "byte 0x41\ndup\napp_global_put", + "app_local_del": "int 0\nbyte 0x41\napp_local_del", + "app_global_del": "byte 0x41\napp_global_del", + "asset_holding_get": "asset_holding_get AssetBalance", + "asset_params_get": "asset_params_get AssetTotal", + } + + excluded := map[string]bool{ + "sha256": true, + "keccak256": true, + "sha512_256": true, + "txn": true, + "gtxn": true, + } + + ep := defaultEvalParams(nil, nil) + + cnt := 0 + for _, spec := range OpSpecs { + if spec.Version > 1 && !excluded[spec.Name] { + source, ok := tests[spec.Name] + require.True(t, ok, fmt.Sprintf("Missed opcode in the test: %s", spec.Name)) + program, err := AssembleString(source) + require.NoError(t, err, source) + // all opcodes allowed in stateful mode so use CheckStateful/EvalStateful + _, err = CheckStateful(program, ep) + require.NoError(t, err, source) + _, _, err = EvalStateful(program, ep) + if spec.Name != "return" { + // "return" opcode is always succeed so ignore it + require.Error(t, err, source) + require.NotContains(t, err.Error(), "illegal opcode") + } + + for v := byte(0); v <= 1; v++ { + program[0] = v + _, err = Check(program, ep) + require.Error(t, err, source) + require.True(t, + strings.Contains(err.Error(), "illegal opcode") || + strings.Contains(err.Error(), "pc did not advance"), + ) + _, err = CheckStateful(program, ep) + require.Error(t, err, source) + require.True(t, + strings.Contains(err.Error(), "illegal opcode") || + strings.Contains(err.Error(), "pc did not advance"), + ) + _, err = Eval(program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + _, _, err = EvalStateful(program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") + } + cnt++ + } + } + require.Equal(t, len(tests), cnt) +} diff --git a/data/transactions/logic/fields.go b/data/transactions/logic/fields.go new file mode 100644 index 0000000000..9158df9659 --- /dev/null +++ b/data/transactions/logic/fields.go @@ -0,0 +1,426 @@ +// 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 . + +package logic + +import ( + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/protocol" +) + +//go:generate stringer -type=TxnField,GlobalField,AssetParamsField,AssetHoldingField,OnCompletionConstType -output=fields_string.go + +// TxnField is an enum type for `txn` and `gtxn` +type TxnField int + +const ( + // Sender Transaction.Sender + Sender TxnField = iota + // Fee Transaction.Fee + Fee + // FirstValid Transaction.FirstValid + FirstValid + // FirstValidTime panic + FirstValidTime + // LastValid Transaction.LastValid + LastValid + // Note Transaction.Note + Note + // Lease Transaction.Lease + Lease + // Receiver Transaction.Receiver + Receiver + // Amount Transaction.Amount + Amount + // CloseRemainderTo Transaction.CloseRemainderTo + CloseRemainderTo + // VotePK Transaction.VotePK + VotePK + // SelectionPK Transaction.SelectionPK + SelectionPK + // VoteFirst Transaction.VoteFirst + VoteFirst + // VoteLast Transaction.VoteLast + VoteLast + // VoteKeyDilution Transaction.VoteKeyDilution + VoteKeyDilution + // Type Transaction.Type + Type + // TypeEnum int(Transaction.Type) + TypeEnum + // XferAsset Transaction.XferAsset + XferAsset + // AssetAmount Transaction.AssetAmount + AssetAmount + // AssetSender Transaction.AssetSender + AssetSender + // AssetReceiver Transaction.AssetReceiver + AssetReceiver + // AssetCloseTo Transaction.AssetCloseTo + AssetCloseTo + // GroupIndex i for txngroup[i] == Txn + GroupIndex + // TxID Transaction.ID() + TxID + // ApplicationID basics.AppIndex + ApplicationID + // OnCompletion OnCompletion + OnCompletion + // ApplicationArgs []basics.TealValue + ApplicationArgs + // NumAppArgs len(ApplicationArgs) + NumAppArgs + // Accounts []basics.Address + Accounts + // NumAccounts len(Accounts) + NumAccounts + // ApprovalProgram []byte + ApprovalProgram + // ClearStateProgram []byte + ClearStateProgram + + invalidTxnField // fence for some setup that loops from Sender..invalidTxnField +) + +// TxnFieldNames are arguments to the 'txn' and 'txnById' opcodes +var TxnFieldNames []string + +// TxnFieldTypes is StackBytes or StackUint64 parallel to TxnFieldNames +var TxnFieldTypes []StackType + +var txnFieldSpecByField map[TxnField]txnFieldSpec +var txnFieldSpecByName tfNameSpecMap + +// simple interface used by doc generator for fields versioning +type tfNameSpecMap map[string]txnFieldSpec + +func (s tfNameSpecMap) getExtraFor(name string) (extra string) { + if s[name].version > 1 { + extra = "LogicSigVersion >= 2." + } + return +} + +type txnFieldSpec struct { + field TxnField + ftype StackType + version uint64 +} + +var txnFieldSpecs = []txnFieldSpec{ + {Sender, StackBytes, 0}, + {Fee, StackUint64, 0}, + {FirstValid, StackUint64, 0}, + {FirstValidTime, StackUint64, 0}, + {LastValid, StackUint64, 0}, + {Note, StackBytes, 0}, + {Lease, StackBytes, 0}, + {Receiver, StackBytes, 0}, + {Amount, StackUint64, 0}, + {CloseRemainderTo, StackBytes, 0}, + {VotePK, StackBytes, 0}, + {SelectionPK, StackBytes, 0}, + {VoteFirst, StackUint64, 0}, + {VoteLast, StackUint64, 0}, + {VoteKeyDilution, StackUint64, 0}, + {Type, StackBytes, 0}, + {TypeEnum, StackUint64, 0}, + {XferAsset, StackUint64, 0}, + {AssetAmount, StackUint64, 0}, + {AssetSender, StackBytes, 0}, + {AssetReceiver, StackBytes, 0}, + {AssetCloseTo, StackBytes, 0}, + {GroupIndex, StackUint64, 0}, + {TxID, StackBytes, 0}, + {ApplicationID, StackUint64, 2}, + {OnCompletion, StackUint64, 2}, + {ApplicationArgs, StackBytes, 2}, + {NumAppArgs, StackUint64, 2}, + {Accounts, StackBytes, 2}, + {NumAccounts, StackUint64, 2}, + {ApprovalProgram, StackBytes, 2}, + {ClearStateProgram, StackBytes, 2}, +} + +// TxnTypeNames is the values of Txn.Type in enum order +var TxnTypeNames = []string{ + string(protocol.UnknownTx), + string(protocol.PaymentTx), + string(protocol.KeyRegistrationTx), + string(protocol.AssetConfigTx), + string(protocol.AssetTransferTx), + string(protocol.AssetFreezeTx), + string(protocol.ApplicationCallTx), +} + +// map TxnTypeName to its enum index, for `txn TypeEnum` +var txnTypeIndexes map[string]int + +// map symbolic name to uint64 for assembleInt +var txnTypeConstToUint64 map[string]uint64 + +// OnCompletionConstType is the same as transactions.OnCompletion +type OnCompletionConstType transactions.OnCompletion + +const ( + // NoOp = transactions.NoOpOC + NoOp OnCompletionConstType = OnCompletionConstType(transactions.NoOpOC) + // OptIn = transactions.OptInOC + OptIn OnCompletionConstType = OnCompletionConstType(transactions.OptInOC) + // CloseOut = transactions.CloseOutOC + CloseOut OnCompletionConstType = OnCompletionConstType(transactions.CloseOutOC) + // ClearState = transactions.ClearStateOC + ClearState OnCompletionConstType = OnCompletionConstType(transactions.ClearStateOC) + // UpdateApplication = transactions.UpdateApplicationOC + UpdateApplication OnCompletionConstType = OnCompletionConstType(transactions.UpdateApplicationOC) + // DeleteApplication = transactions.DeleteApplicationOC + DeleteApplication OnCompletionConstType = OnCompletionConstType(transactions.DeleteApplicationOC) + // end of constants + invalidOnCompletionConst OnCompletionConstType = DeleteApplication + 1 +) + +// OnCompletionNames is the string names of Txn.OnCompletion, array index is the const value +var OnCompletionNames []string + +// onCompletionConstToUint64 map symbolic name to uint64 for assembleInt +var onCompletionConstToUint64 map[string]uint64 + +// GlobalField is an enum for `global` opcode +type GlobalField int + +const ( + // MinTxnFee ConsensusParams.MinTxnFee + MinTxnFee GlobalField = iota + // MinBalance ConsensusParams.MinBalance + MinBalance + // MaxTxnLife ConsensusParams.MaxTxnLife + MaxTxnLife + // ZeroAddress [32]byte{0...} + ZeroAddress + // GroupSize len(txn group) + GroupSize + // LogicSigVersion ConsensusParams.LogicSigVersion + LogicSigVersion + // Round basics.Round + Round + // LatestTimestamp uint64 + LatestTimestamp + + invalidGlobalField +) + +// GlobalFieldNames are arguments to the 'global' opcode +var GlobalFieldNames []string + +// GlobalFieldTypes is StackUint64 StackBytes in parallel with GlobalFieldNames +var GlobalFieldTypes []StackType + +type globalFieldSpec struct { + gfield GlobalField + ftype StackType + mode runMode + version uint64 +} + +var globalFieldSpecs = []globalFieldSpec{ + {MinTxnFee, StackUint64, modeAny, 0}, // version 0 is the same as TEAL v1 (initial TEAL release) + {MinBalance, StackUint64, modeAny, 0}, + {MaxTxnLife, StackUint64, modeAny, 0}, + {ZeroAddress, StackBytes, modeAny, 0}, + {GroupSize, StackUint64, modeAny, 0}, + {LogicSigVersion, StackUint64, modeAny, 2}, + {Round, StackUint64, runModeApplication, 2}, + {LatestTimestamp, StackUint64, runModeApplication, 2}, +} + +// GlobalFieldSpecByField maps GlobalField to spec +var globalFieldSpecByField map[GlobalField]globalFieldSpec +var globalFieldSpecByName gfNameSpecMap + +// simple interface used by doc generator for fields versioning +type gfNameSpecMap map[string]globalFieldSpec + +func (s gfNameSpecMap) getExtraFor(name string) (extra string) { + if s[name].version > 1 { + extra = "LogicSigVersion >= 2." + } + return +} + +// AssetHoldingField is an enum for `asset_holding_get` opcode +type AssetHoldingField int + +const ( + // AssetBalance AssetHolding.Amount + AssetBalance AssetHoldingField = iota + // AssetFrozen AssetHolding.Frozen + AssetFrozen + invalidAssetHoldingField +) + +// AssetHoldingFieldNames are arguments to the 'asset_holding_get' opcode +var AssetHoldingFieldNames []string + +type assetHoldingFieldType struct { + field AssetHoldingField + ftype StackType +} + +var assetHoldingFieldTypeList = []assetHoldingFieldType{ + {AssetBalance, StackUint64}, + {AssetFrozen, StackUint64}, +} + +// AssetHoldingFieldTypes is StackUint64 StackBytes in parallel with AssetHoldingFieldNames +var AssetHoldingFieldTypes []StackType + +var assetHoldingFields map[string]uint + +// AssetParamsField is an enum for `asset_params_get` opcode +type AssetParamsField int + +const ( + // AssetTotal AssetParams.Total + AssetTotal AssetParamsField = iota + // AssetDecimals AssetParams.Decimals + AssetDecimals + // AssetDefaultFrozen AssetParams.AssetDefaultFrozen + AssetDefaultFrozen + // AssetUnitName AssetParams.UnitName + AssetUnitName + // AssetAssetName AssetParams.AssetName + AssetAssetName + // AssetURL AssetParams.URL + AssetURL + // AssetMetadataHash AssetParams.MetadataHash + AssetMetadataHash + // AssetManager AssetParams.Manager + AssetManager + // AssetReserve AssetParams.Reserve + AssetReserve + // AssetFreeze AssetParams.Freeze + AssetFreeze + // AssetClawback AssetParams.Clawback + AssetClawback + invalidAssetParamsField +) + +// AssetParamsFieldNames are arguments to the 'asset_holding_get' opcode +var AssetParamsFieldNames []string + +type assetParamsFieldType struct { + field AssetParamsField + ftype StackType +} + +var assetParamsFieldTypeList = []assetParamsFieldType{ + {AssetTotal, StackUint64}, + {AssetDecimals, StackUint64}, + {AssetDefaultFrozen, StackUint64}, + {AssetUnitName, StackBytes}, + {AssetAssetName, StackBytes}, + {AssetURL, StackBytes}, + {AssetMetadataHash, StackBytes}, + {AssetManager, StackBytes}, + {AssetReserve, StackBytes}, + {AssetFreeze, StackBytes}, + {AssetClawback, StackBytes}, +} + +// AssetParamsFieldTypes is StackUint64 StackBytes in parallel with AssetParamsFieldNames +var AssetParamsFieldTypes []StackType + +var assetParamsFields map[string]uint + +func init() { + TxnFieldNames = make([]string, int(invalidTxnField)) + for fi := Sender; fi < invalidTxnField; fi++ { + TxnFieldNames[fi] = fi.String() + } + TxnFieldTypes = make([]StackType, int(invalidTxnField)) + txnFieldSpecByField = make(map[TxnField]txnFieldSpec, len(TxnFieldNames)) + for i, s := range txnFieldSpecs { + if int(s.field) != i { + panic("txnFieldTypePairs disjoint with TxnField enum") + } + TxnFieldTypes[i] = s.ftype + txnFieldSpecByField[s.field] = s + } + txnFieldSpecByName = make(tfNameSpecMap, len(TxnFieldNames)) + for i, tfn := range TxnFieldNames { + txnFieldSpecByName[tfn] = txnFieldSpecByField[TxnField(i)] + } + + GlobalFieldNames = make([]string, int(invalidGlobalField)) + for i := MinTxnFee; i < invalidGlobalField; i++ { + GlobalFieldNames[int(i)] = i.String() + } + GlobalFieldTypes = make([]StackType, len(GlobalFieldNames)) + globalFieldSpecByField = make(map[GlobalField]globalFieldSpec, len(GlobalFieldNames)) + for _, s := range globalFieldSpecs { + GlobalFieldTypes[int(s.gfield)] = s.ftype + globalFieldSpecByField[s.gfield] = s + } + globalFieldSpecByName = make(gfNameSpecMap, len(GlobalFieldNames)) + for i, gfn := range GlobalFieldNames { + globalFieldSpecByName[gfn] = globalFieldSpecByField[GlobalField(i)] + } + + AssetHoldingFieldNames = make([]string, int(invalidAssetHoldingField)) + for i := AssetBalance; i < invalidAssetHoldingField; i++ { + AssetHoldingFieldNames[int(i)] = i.String() + } + AssetHoldingFieldTypes = make([]StackType, len(AssetHoldingFieldNames)) + for _, ft := range assetHoldingFieldTypeList { + AssetHoldingFieldTypes[int(ft.field)] = ft.ftype + } + assetHoldingFields = make(map[string]uint) + for i, fn := range AssetHoldingFieldNames { + assetHoldingFields[fn] = uint(i) + } + + AssetParamsFieldNames = make([]string, int(invalidAssetParamsField)) + for i := AssetTotal; i < invalidAssetParamsField; i++ { + AssetParamsFieldNames[int(i)] = i.String() + } + AssetParamsFieldTypes = make([]StackType, len(AssetParamsFieldNames)) + for _, ft := range assetParamsFieldTypeList { + AssetParamsFieldTypes[int(ft.field)] = ft.ftype + } + assetParamsFields = make(map[string]uint) + for i, fn := range AssetParamsFieldNames { + assetParamsFields[fn] = uint(i) + } + + txnTypeIndexes = make(map[string]int, len(TxnTypeNames)) + for i, tt := range TxnTypeNames { + txnTypeIndexes[tt] = i + } + + txnTypeConstToUint64 = make(map[string]uint64, len(TxnTypeNames)) + for tt, v := range txnTypeIndexes { + symbol := TypeNameDescription(tt) + txnTypeConstToUint64[symbol] = uint64(v) + } + + OnCompletionNames = make([]string, int(invalidOnCompletionConst)) + onCompletionConstToUint64 = make(map[string]uint64, len(OnCompletionNames)) + for oc := NoOp; oc < invalidOnCompletionConst; oc++ { + symbol := oc.String() + OnCompletionNames[oc] = symbol + onCompletionConstToUint64[symbol] = uint64(oc) + } +} diff --git a/data/transactions/logic/fields_string.go b/data/transactions/logic/fields_string.go new file mode 100644 index 0000000000..0617b686e3 --- /dev/null +++ b/data/transactions/logic/fields_string.go @@ -0,0 +1,150 @@ +// Code generated by "stringer -type=TxnField,GlobalField,AssetParamsField,AssetHoldingField,OnCompletionConstType -output=fields_string.go"; DO NOT EDIT. + +package logic + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Sender-0] + _ = x[Fee-1] + _ = x[FirstValid-2] + _ = x[FirstValidTime-3] + _ = x[LastValid-4] + _ = x[Note-5] + _ = x[Lease-6] + _ = x[Receiver-7] + _ = x[Amount-8] + _ = x[CloseRemainderTo-9] + _ = x[VotePK-10] + _ = x[SelectionPK-11] + _ = x[VoteFirst-12] + _ = x[VoteLast-13] + _ = x[VoteKeyDilution-14] + _ = x[Type-15] + _ = x[TypeEnum-16] + _ = x[XferAsset-17] + _ = x[AssetAmount-18] + _ = x[AssetSender-19] + _ = x[AssetReceiver-20] + _ = x[AssetCloseTo-21] + _ = x[GroupIndex-22] + _ = x[TxID-23] + _ = x[ApplicationID-24] + _ = x[OnCompletion-25] + _ = x[ApplicationArgs-26] + _ = x[NumAppArgs-27] + _ = x[Accounts-28] + _ = x[NumAccounts-29] + _ = x[ApprovalProgram-30] + _ = x[ClearStateProgram-31] + _ = x[invalidTxnField-32] +} + +const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDApplicationIDOnCompletionApplicationArgsNumAppArgsAccountsNumAccountsApprovalProgramClearStatePrograminvalidTxnField" + +var _TxnField_index = [...]uint16{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 225, 237, 252, 262, 270, 281, 296, 313, 328} + +func (i TxnField) String() string { + if i < 0 || i >= TxnField(len(_TxnField_index)-1) { + return "TxnField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TxnField_name[_TxnField_index[i]:_TxnField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[MinTxnFee-0] + _ = x[MinBalance-1] + _ = x[MaxTxnLife-2] + _ = x[ZeroAddress-3] + _ = x[GroupSize-4] + _ = x[LogicSigVersion-5] + _ = x[Round-6] + _ = x[LatestTimestamp-7] + _ = x[invalidGlobalField-8] +} + +const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeLogicSigVersionRoundLatestTimestampinvalidGlobalField" + +var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 64, 69, 84, 102} + +func (i GlobalField) String() string { + if i < 0 || i >= GlobalField(len(_GlobalField_index)-1) { + return "GlobalField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _GlobalField_name[_GlobalField_index[i]:_GlobalField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[AssetTotal-0] + _ = x[AssetDecimals-1] + _ = x[AssetDefaultFrozen-2] + _ = x[AssetUnitName-3] + _ = x[AssetAssetName-4] + _ = x[AssetURL-5] + _ = x[AssetMetadataHash-6] + _ = x[AssetManager-7] + _ = x[AssetReserve-8] + _ = x[AssetFreeze-9] + _ = x[AssetClawback-10] + _ = x[invalidAssetParamsField-11] +} + +const _AssetParamsField_name = "AssetTotalAssetDecimalsAssetDefaultFrozenAssetUnitNameAssetAssetNameAssetURLAssetMetadataHashAssetManagerAssetReserveAssetFreezeAssetClawbackinvalidAssetParamsField" + +var _AssetParamsField_index = [...]uint8{0, 10, 23, 41, 54, 68, 76, 93, 105, 117, 128, 141, 164} + +func (i AssetParamsField) String() string { + if i < 0 || i >= AssetParamsField(len(_AssetParamsField_index)-1) { + return "AssetParamsField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _AssetParamsField_name[_AssetParamsField_index[i]:_AssetParamsField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[AssetBalance-0] + _ = x[AssetFrozen-1] + _ = x[invalidAssetHoldingField-2] +} + +const _AssetHoldingField_name = "AssetBalanceAssetFrozeninvalidAssetHoldingField" + +var _AssetHoldingField_index = [...]uint8{0, 12, 23, 47} + +func (i AssetHoldingField) String() string { + if i < 0 || i >= AssetHoldingField(len(_AssetHoldingField_index)-1) { + return "AssetHoldingField(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _AssetHoldingField_name[_AssetHoldingField_index[i]:_AssetHoldingField_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[NoOp-0] + _ = x[OptIn-1] + _ = x[CloseOut-2] + _ = x[ClearState-3] + _ = x[UpdateApplication-4] + _ = x[DeleteApplication-5] + _ = x[invalidOnCompletionConst-6] +} + +const _OnCompletionConstType_name = "NoOpOptInCloseOutClearStateUpdateApplicationDeleteApplicationinvalidOnCompletionConst" + +var _OnCompletionConstType_index = [...]uint8{0, 4, 9, 17, 27, 44, 61, 85} + +func (i OnCompletionConstType) String() string { + if i >= OnCompletionConstType(len(_OnCompletionConstType_index)-1) { + return "OnCompletionConstType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _OnCompletionConstType_name[_OnCompletionConstType_index[i]:_OnCompletionConstType_index[i+1]] +} diff --git a/data/transactions/logic/globalfield_string.go b/data/transactions/logic/globalfield_string.go deleted file mode 100644 index 8a1d3698db..0000000000 --- a/data/transactions/logic/globalfield_string.go +++ /dev/null @@ -1,28 +0,0 @@ -// Code generated by "stringer -type=GlobalField"; DO NOT EDIT. - -package logic - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[MinTxnFee-0] - _ = x[MinBalance-1] - _ = x[MaxTxnLife-2] - _ = x[ZeroAddress-3] - _ = x[GroupSize-4] - _ = x[invalidGlobalField-5] -} - -const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeinvalidGlobalField" - -var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 67} - -func (i GlobalField) String() string { - if i < 0 || i >= GlobalField(len(_GlobalField_index)-1) { - return "GlobalField(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _GlobalField_name[_GlobalField_index[i]:_GlobalField_index[i+1]] -} diff --git a/data/transactions/logic/kvcow.go b/data/transactions/logic/kvcow.go new file mode 100644 index 0000000000..011970b986 --- /dev/null +++ b/data/transactions/logic/kvcow.go @@ -0,0 +1,73 @@ +// 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 . + +package logic + +import ( + "github.com/algorand/go-algorand/data/basics" +) + +type keyValueCow struct { + base basics.TealKeyValue + delta basics.StateDelta +} + +func makeKeyValueCow(base basics.TealKeyValue, delta basics.StateDelta) *keyValueCow { + var kvc keyValueCow + kvc.base = base + kvc.delta = delta + return &kvc +} + +func (kvc *keyValueCow) read(key string) (value basics.TealValue, ok bool) { + // If the value for the key has been modified in the delta, + // then return the modified value. + valueDelta, ok := kvc.delta[key] + if ok { + return valueDelta.ToTealValue() + } + + // Otherwise, return the value from the underlying key/value. + value, ok = kvc.base[key] + return value, ok +} + +func (kvc *keyValueCow) write(key string, value basics.TealValue) { + // If the value being written is identical to the underlying key/value, + // then ensure there is no delta entry for the key. + baseValue, ok := kvc.base[key] + if ok && value == baseValue { + delete(kvc.delta, key) + } else { + // Otherwise, update the delta with the new value. + kvc.delta[key] = value.ToValueDelta() + } +} + +func (kvc *keyValueCow) del(key string) { + _, ok := kvc.base[key] + if ok { + // If the key already exists in the underlying key/value, + // update the delta to indicate that the value was deleted. + kvc.delta[key] = basics.ValueDelta{ + Action: basics.DeleteAction, + } + } else { + // Since the key didn't exist in the underlying key/value, + // don't include a delta entry for its deletion. + delete(kvc.delta, key) + } +} diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go new file mode 100644 index 0000000000..3228b32188 --- /dev/null +++ b/data/transactions/logic/opcodes.go @@ -0,0 +1,259 @@ +// 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 . + +package logic + +import ( + "sort" +) + +// LogicVersion defines default assembler and max eval versions +const LogicVersion = 2 + +// opSize records the length in bytes for an op that is constant-length but not length 1 +type opSize struct { + cost int + size int + checkFunc opCheckFunc +} + +var opSizeDefault = opSize{1, 1, nil} + +// OpSpec defines one byte opcode +type OpSpec struct { + Opcode byte + Name string + op opEvalFunc // evaluate the op + asm assembleFunc // assemble the op + dis disassembleFunc // disassemble the op + Args StackTypes // what gets popped from the stack + Returns StackTypes // what gets pushed to the stack + Version uint64 // TEAL version opcode introduced + Modes runMode // if non-zero, then (mode & Modes) != 0 to allow + opSize opSize // opSizes records the size of ops that are constant size but not 1, time 'cost' and custom check functions. +} + +var oneBytes = StackTypes{StackBytes} +var twoBytes = StackTypes{StackBytes, StackBytes} +var threeBytes = StackTypes{StackBytes, StackBytes, StackBytes} +var byteIntInt = StackTypes{StackBytes, StackUint64, StackUint64} +var oneInt = StackTypes{StackUint64} +var twoInts = StackTypes{StackUint64, StackUint64} +var oneAny = StackTypes{StackAny} +var twoAny = StackTypes{StackAny, StackAny} + +// OpSpecs is the table of operations that can be assembled and evaluated. +// +// Any changes should be reflected in README_in.md which serves as the language spec. +// +// WARNING: special case op assembly by argOps functions must do their own type stack maintenance via ops.tpop() ops.tpush()/ops.tpusha() +var OpSpecs = []OpSpec{ + {0x00, "err", opErr, asmDefault, disDefault, nil, nil, 1, modeAny, opSizeDefault}, + {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{7, 1, nil}}, + {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{26, 1, nil}}, + {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 1, modeAny, opSize{9, 1, nil}}, + + // Cost of these opcodes increases in TEAL version 2 based on measured + // performance. Should be able to run max hashes during stateful TEAL + // and achieve reasonable TPS. Same opcode for different TEAL versions + // is OK. + {0x01, "sha256", opSHA256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{35, 1, nil}}, + {0x02, "keccak256", opKeccak256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{130, 1, nil}}, + {0x03, "sha512_256", opSHA512_256, asmDefault, disDefault, oneBytes, oneBytes, 2, modeAny, opSize{45, 1, nil}}, + + {0x04, "ed25519verify", opEd25519verify, asmDefault, disDefault, threeBytes, oneInt, 1, runModeSignature, opSize{1900, 1, nil}}, + {0x08, "+", opPlus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x09, "-", opMinus, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0a, "/", opDiv, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0b, "*", opMul, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0c, "<", opLt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0d, ">", opGt, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0e, "<=", opLe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x0f, ">=", opGe, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x10, "&&", opAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x11, "||", opOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x12, "==", opEq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opSizeDefault}, + {0x13, "!=", opNeq, asmDefault, disDefault, twoAny, oneInt, 1, modeAny, opSizeDefault}, + {0x14, "!", opNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opSizeDefault}, + {0x15, "len", opLen, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opSizeDefault}, + {0x16, "itob", opItob, asmDefault, disDefault, oneInt, oneBytes, 1, modeAny, opSizeDefault}, + {0x17, "btoi", opBtoi, asmDefault, disDefault, oneBytes, oneInt, 1, modeAny, opSizeDefault}, + {0x18, "%", opModulo, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x19, "|", opBitOr, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x1a, "&", opBitAnd, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x1b, "^", opBitXor, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, + {0x1c, "~", opBitNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opSizeDefault}, + {0x1d, "mulw", opMulw, asmDefault, disDefault, twoInts, twoInts, 1, modeAny, opSizeDefault}, + + {0x20, "intcblock", opIntConstBlock, assembleIntCBlock, disIntcblock, nil, nil, 1, modeAny, opSize{1, 0, checkIntConstBlock}}, + {0x21, "intc", opIntConstLoad, assembleIntC, disIntc, nil, oneInt, 1, modeAny, opSize{1, 2, nil}}, + {0x22, "intc_0", opIntConst0, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, + {0x23, "intc_1", opIntConst1, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, + {0x24, "intc_2", opIntConst2, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, + {0x25, "intc_3", opIntConst3, asmDefault, disDefault, nil, oneInt, 1, modeAny, opSizeDefault}, + {0x26, "bytecblock", opByteConstBlock, assembleByteCBlock, disBytecblock, nil, nil, 1, modeAny, opSize{1, 0, checkByteConstBlock}}, + {0x27, "bytec", opByteConstLoad, assembleByteC, disBytec, nil, oneBytes, 1, modeAny, opSize{1, 2, nil}}, + {0x28, "bytec_0", opByteConst0, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, + {0x29, "bytec_1", opByteConst1, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, + {0x2a, "bytec_2", opByteConst2, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, + {0x2b, "bytec_3", opByteConst3, asmDefault, disDefault, nil, oneBytes, 1, modeAny, opSizeDefault}, + {0x2c, "arg", opArg, assembleArg, disArg, nil, oneBytes, 1, runModeSignature, opSize{1, 2, nil}}, + {0x2d, "arg_0", opArg0, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, + {0x2e, "arg_1", opArg1, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, + {0x2f, "arg_2", opArg2, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, + {0x30, "arg_3", opArg3, asmDefault, disDefault, nil, oneBytes, 1, runModeSignature, opSizeDefault}, + {0x31, "txn", opTxn, assembleTxn, disTxn, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, + // It is ok to have the same opcode for different TEAL versions. + // This 'txn' asm command supports additional argument in version 2 and + // generates 'txna' opcode in that particular case + {0x31, "txn", opTxn, assembleTxn2, disTxn, nil, oneAny, 2, modeAny, opSize{1, 2, nil}}, + {0x32, "global", opGlobal, assembleGlobal, disGlobal, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, + {0x33, "gtxn", opGtxn, assembleGtxn, disGtxn, nil, oneAny, 1, modeAny, opSize{1, 3, nil}}, + {0x33, "gtxn", opGtxn, assembleGtxn2, disGtxn, nil, oneAny, 2, modeAny, opSize{1, 3, nil}}, + {0x34, "load", opLoad, assembleLoad, disLoad, nil, oneAny, 1, modeAny, opSize{1, 2, nil}}, + {0x35, "store", opStore, assembleStore, disStore, oneAny, nil, 1, modeAny, opSize{1, 2, nil}}, + {0x36, "txna", opTxna, assembleTxna, disTxna, nil, oneAny, 2, modeAny, opSize{1, 3, nil}}, + {0x37, "gtxna", opGtxna, assembleGtxna, disGtxna, nil, oneAny, 2, modeAny, opSize{1, 4, nil}}, + + {0x40, "bnz", opBnz, assembleBranch, disBranch, oneInt, nil, 1, modeAny, opSize{1, 3, checkBranch}}, + {0x41, "bz", opBz, assembleBranch, disBranch, oneInt, nil, 2, modeAny, opSize{1, 3, checkBranch}}, + {0x42, "b", opB, assembleBranch, disBranch, nil, nil, 2, modeAny, opSize{1, 3, checkBranch}}, + {0x43, "return", opReturn, asmDefault, disDefault, oneInt, nil, 2, modeAny, opSizeDefault}, + {0x48, "pop", opPop, asmDefault, disDefault, oneAny, nil, 1, modeAny, opSizeDefault}, + {0x49, "dup", opDup, asmDefault, disDefault, oneAny, twoAny, 1, modeAny, opSizeDefault}, + {0x4a, "dup2", opDup2, asmDefault, disDefault, twoAny, twoAny.plus(twoAny), 2, modeAny, opSizeDefault}, + + {0x50, "concat", opConcat, asmDefault, disDefault, twoBytes, oneBytes, 2, modeAny, opSizeDefault}, + {0x51, "substring", opSubstring, assembleSubstring, disSubstring, oneBytes, oneBytes, 2, modeAny, opSize{1, 3, nil}}, + {0x52, "substring3", opSubstring3, asmDefault, disDefault, byteIntInt, oneBytes, 2, modeAny, opSizeDefault}, + + {0x60, "balance", opBalance, asmDefault, disDefault, oneInt, oneInt, 2, runModeApplication, opSizeDefault}, + {0x61, "app_opted_in", opAppCheckOptedIn, asmDefault, disDefault, twoInts, oneInt, 2, runModeApplication, opSizeDefault}, + {0x62, "app_local_get", opAppGetLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), oneAny, 2, runModeApplication, opSizeDefault}, + {0x63, "app_local_get_ex", opAppGetLocalStateEx, asmDefault, disDefault, twoInts.plus(oneBytes), oneInt.plus(oneAny), 2, runModeApplication, opSizeDefault}, + {0x64, "app_global_get", opAppGetGlobalState, asmDefault, disDefault, oneBytes, oneAny, 2, runModeApplication, opSizeDefault}, + {0x65, "app_global_get_ex", opAppGetGlobalStateEx, asmDefault, disDefault, oneInt.plus(oneBytes), oneInt.plus(oneAny), 2, runModeApplication, opSizeDefault}, + {0x66, "app_local_put", opAppPutLocalState, asmDefault, disDefault, oneInt.plus(oneBytes).plus(oneAny), nil, 2, runModeApplication, opSizeDefault}, + {0x67, "app_global_put", opAppPutGlobalState, asmDefault, disDefault, oneBytes.plus(oneAny), nil, 2, runModeApplication, opSizeDefault}, + {0x68, "app_local_del", opAppDeleteLocalState, asmDefault, disDefault, oneInt.plus(oneBytes), nil, 2, runModeApplication, opSizeDefault}, + {0x69, "app_global_del", opAppDeleteGlobalState, asmDefault, disDefault, oneBytes, nil, 2, runModeApplication, opSizeDefault}, + + {0x70, "asset_holding_get", opAssetHoldingGet, assembleAssetHolding, disAssetHolding, twoInts, oneInt.plus(oneAny), 2, runModeApplication, opSize{1, 2, nil}}, + {0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, twoInts, oneInt.plus(oneAny), 2, runModeApplication, opSize{1, 2, nil}}, +} + +type sortByOpcode []OpSpec + +func (a sortByOpcode) Len() int { return len(a) } +func (a sortByOpcode) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByOpcode) Less(i, j int) bool { return a[i].Opcode < a[j].Opcode } + +// OpcodesByVersion returns list of opcodes available in a specific version of TEAL +// by copying v1 opcodes to v2 to create a full list. +// This function must be used for documentation only because it modifies opcode versions +// to the first introduced for the opcodes updated in later versions. +func OpcodesByVersion(version uint64) []OpSpec { + // for updated opcodes use the lowest version opcode was introduced in + maxOpcode := 0 + for i := 0; i < len(OpSpecs); i++ { + if int(OpSpecs[i].Opcode) > maxOpcode { + maxOpcode = int(OpSpecs[i].Opcode) + } + } + updated := make([]int, maxOpcode+1) + for idx := range OpSpecs { + op := OpSpecs[idx].Opcode + cv := updated[op] + if cv == 0 { + cv = int(OpSpecs[idx].Version) + } else { + if int(OpSpecs[idx].Version) < cv { + cv = int(OpSpecs[idx].Version) + } + } + updated[op] = cv + } + + subv := make(map[byte]OpSpec) + for idx := range OpSpecs { + if OpSpecs[idx].Version <= version { + op := OpSpecs[idx].Opcode + subv[op] = OpSpecs[idx] + // if the opcode was updated then assume backward compatibility + // and set version to minimum available + if updated[op] < int(OpSpecs[idx].Version) { + copy := OpSpecs[idx] + copy.Version = uint64(updated[op]) + subv[op] = copy + } + } + } + result := make([]OpSpec, 0, len(subv)) + for _, v := range subv { + result = append(result, v) + } + sort.Sort(sortByOpcode(result)) + return result +} + +// direct opcode bytes +var opsByOpcode [LogicVersion + 1][256]OpSpec +var opsByName [LogicVersion + 1]map[string]OpSpec + +// Migration from TEAL v1 to TEAL v2. +// TEAL v1 allowed execution of program with version 0. +// With TEAL v2 opcode versions are introduced and they are bound to every opcode. +// There is no opcodes with version 0 so that TEAL v2 evaluator rejects any program with version 0. +// To preserve backward compatibility version 0 array is populated with TEAL v1 opcodes +// with the version overwritten to 0. +func init() { + // First, initialize baseline v1 opcodes. + // Zero (empty) version is an alias for TEAL v1 opcodes and needed for compatibility with v1 code. + opsByName[0] = make(map[string]OpSpec, 256) + opsByName[1] = make(map[string]OpSpec, 256) + for _, oi := range OpSpecs { + if oi.Version == 1 { + cp := oi + cp.Version = 0 + opsByOpcode[0][oi.Opcode] = cp + opsByName[0][oi.Name] = cp + + opsByOpcode[1][oi.Opcode] = oi + opsByName[1][oi.Name] = oi + } + } + // Start from v2 TEAL and higher, + // copy lower version opcodes and overwrite matching version + for v := uint64(2); v <= EvalMaxVersion; v++ { + opsByName[v] = make(map[string]OpSpec, 256) + + // Copy opcodes from lower version + for opName, oi := range opsByName[v-1] { + opsByName[v][opName] = oi + } + for op, oi := range opsByOpcode[v-1] { + opsByOpcode[v][op] = oi + } + + // Update tables with opcodes from the current version + for _, oi := range OpSpecs { + if oi.Version == v { + opsByOpcode[v][oi.Opcode] = oi + opsByName[v][oi.Name] = oi + } + } + } +} diff --git a/data/transactions/logic/opcodes_test.go b/data/transactions/logic/opcodes_test.go new file mode 100644 index 0000000000..58d6c78659 --- /dev/null +++ b/data/transactions/logic/opcodes_test.go @@ -0,0 +1,152 @@ +// 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 . + +package logic + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOpSpecs(t *testing.T) { + t.Parallel() + + for _, spec := range OpSpecs { + require.NotEmpty(t, spec.opSize, spec) + } +} + +func TestOpcodesByVersion(t *testing.T) { + t.Parallel() + + opSpecs := make([][]OpSpec, 2) + for v := uint64(1); v <= LogicVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + opSpecs[v-1] = OpcodesByVersion(v) + isOk := true + for i := 0; i < len(opSpecs[v-1])-1; i++ { + cur := opSpecs[v-1][i] + next := opSpecs[v-1][i+1] + // check duplicates + if cur.Opcode == next.Opcode { + isOk = false + break + } + // check sorted + if cur.Opcode > next.Opcode { + isOk = false + break + } + + } + require.True(t, isOk) + }) + } + require.Greater(t, len(opSpecs[1]), len(opSpecs[0])) +} + +func TestOpcodesVersioningV2(t *testing.T) { + t.Parallel() + + require.Equal(t, 3, len(opsByOpcode)) + require.Equal(t, 3, len(opsByName)) + + // ensure v0 has only v0 opcodes + cntv0 := 0 + for _, spec := range opsByOpcode[0] { + if spec.op != nil { + require.Equal(t, uint64(0), spec.Version) + cntv0++ + } + } + for _, spec := range opsByName[0] { + if spec.op != nil { + require.Equal(t, uint64(0), spec.Version) + } + } + require.Equal(t, cntv0, len(opsByName[0])) + + // ensure v1 has only v1 opcodes + cntv1 := 0 + for _, spec := range opsByOpcode[1] { + if spec.op != nil { + require.Equal(t, uint64(1), spec.Version, spec) + cntv1++ + } + } + for _, spec := range opsByName[1] { + if spec.op != nil { + require.Equal(t, uint64(1), spec.Version) + } + } + require.Equal(t, cntv1, len(opsByName[1])) + require.Equal(t, cntv1, cntv0) + require.Equal(t, 52, cntv1) + + eqButVersion := func(a *OpSpec, b *OpSpec) (eq bool) { + eq = a.Opcode == b.Opcode && a.Name == b.Name && + reflect.ValueOf(a.op).Pointer() == reflect.ValueOf(b.op).Pointer() && + reflect.ValueOf(a.asm).Pointer() == reflect.ValueOf(b.asm).Pointer() && + reflect.ValueOf(a.dis).Pointer() == reflect.ValueOf(b.dis).Pointer() && + reflect.DeepEqual(a.Args, b.Args) && reflect.DeepEqual(a.Returns, b.Returns) && + a.Modes == b.Modes && + a.opSize.cost == b.opSize.cost && a.opSize.size == b.opSize.size && + reflect.ValueOf(a.opSize.checkFunc).Pointer() == reflect.ValueOf(b.opSize.checkFunc).Pointer() + return + } + // ensure v0 and v1 are the same + require.Equal(t, len(opsByOpcode[1]), len(opsByOpcode[0])) + require.Equal(t, len(opsByName[1]), len(opsByName[0])) + for op, spec1 := range opsByOpcode[1] { + spec0 := opsByOpcode[0][op] + msg := fmt.Sprintf("%v\n%v\n", spec0, spec1) + require.True(t, eqButVersion(&spec1, &spec0), msg) + } + for name, spec1 := range opsByName[1] { + spec0 := opsByName[0][name] + require.True(t, eqButVersion(&spec1, &spec0)) + } + + // ensure v2 has v1 and v2 opcodes + require.Equal(t, len(opsByName[2]), len(opsByName[2])) + cntv2 := 0 + cntAdded := 0 + for _, spec := range opsByOpcode[2] { + if spec.op != nil { + require.True(t, spec.Version == 1 || spec.Version == 2) + if spec.Version == 2 { + cntAdded++ + } + cntv2++ + } + } + for _, spec := range opsByName[2] { + if spec.op != nil { + require.True(t, spec.Version == 1 || spec.Version == 2) + } + } + require.Equal(t, cntv2, len(opsByName[2])) + + // hardcode and ensure amount of new v2 opcodes + newOpcodes := 21 + overwritten := 5 // sha256, keccak256, sha512_256, txn, gtxn + require.Equal(t, newOpcodes+overwritten, cntAdded) + + require.Equal(t, cntv2, cntv1+newOpcodes) +} diff --git a/data/transactions/logic/txnfield_string.go b/data/transactions/logic/txnfield_string.go deleted file mode 100644 index 7cbf8da6c6..0000000000 --- a/data/transactions/logic/txnfield_string.go +++ /dev/null @@ -1,47 +0,0 @@ -// Code generated by "stringer -type=TxnField"; DO NOT EDIT. - -package logic - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[Sender-0] - _ = x[Fee-1] - _ = x[FirstValid-2] - _ = x[FirstValidTime-3] - _ = x[LastValid-4] - _ = x[Note-5] - _ = x[Lease-6] - _ = x[Receiver-7] - _ = x[Amount-8] - _ = x[CloseRemainderTo-9] - _ = x[VotePK-10] - _ = x[SelectionPK-11] - _ = x[VoteFirst-12] - _ = x[VoteLast-13] - _ = x[VoteKeyDilution-14] - _ = x[Type-15] - _ = x[TypeEnum-16] - _ = x[XferAsset-17] - _ = x[AssetAmount-18] - _ = x[AssetSender-19] - _ = x[AssetReceiver-20] - _ = x[AssetCloseTo-21] - _ = x[GroupIndex-22] - _ = x[TxID-23] - _ = x[invalidTxnField-24] -} - -const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDinvalidTxnField" - -var _TxnField_index = [...]uint8{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 227} - -func (i TxnField) String() string { - if i < 0 || i >= TxnField(len(_TxnField_index)-1) { - return "TxnField(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _TxnField_name[_TxnField_index[i]:_TxnField_index[i+1]] -} diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index 1ea652529c..623ebc4036 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -4,10 +4,19 @@ package transactions import ( "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/msgp/msgp" ) // The following msgp objects are implemented in this file: +// ApplicationCallTxnFields +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // ApplyData // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -72,6 +81,14 @@ import ( // |-----> Msgsize // |-----> MsgIsZero // +// OnCompletion +// |-----> MarshalMsg +// |-----> CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> Msgsize +// |-----> MsgIsZero +// // PaymentTxnFields // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -137,28 +154,509 @@ import ( // |-----> (*) MsgIsZero // +// MarshalMsg implements msgp.Marshaler +func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0004Len := uint32(9) + var zb0004Mask uint16 /* 10 bits */ + if len((*z).ApplicationArgs) == 0 { + zb0004Len-- + zb0004Mask |= 0x2 + } + if (*z).OnCompletion == 0 { + zb0004Len-- + zb0004Mask |= 0x4 + } + if len((*z).ApprovalProgram) == 0 { + zb0004Len-- + zb0004Mask |= 0x8 + } + if len((*z).Accounts) == 0 { + zb0004Len-- + zb0004Mask |= 0x10 + } + if len((*z).ForeignApps) == 0 { + zb0004Len-- + zb0004Mask |= 0x20 + } + if (*z).GlobalStateSchema.MsgIsZero() { + zb0004Len-- + zb0004Mask |= 0x40 + } + if (*z).ApplicationID.MsgIsZero() { + zb0004Len-- + zb0004Mask |= 0x80 + } + if (*z).LocalStateSchema.MsgIsZero() { + zb0004Len-- + zb0004Mask |= 0x100 + } + if len((*z).ClearStateProgram) == 0 { + zb0004Len-- + zb0004Mask |= 0x200 + } + // variable map header, size zb0004Len + o = append(o, 0x80|uint8(zb0004Len)) + if zb0004Len != 0 { + if (zb0004Mask & 0x2) == 0 { // if not empty + // string "apaa" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x61) + if (*z).ApplicationArgs == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ApplicationArgs))) + } + for zb0001 := range (*z).ApplicationArgs { + o = msgp.AppendBytes(o, (*z).ApplicationArgs[zb0001]) + } + } + if (zb0004Mask & 0x4) == 0 { // if not empty + // string "apan" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x6e) + o = msgp.AppendUint64(o, uint64((*z).OnCompletion)) + } + if (zb0004Mask & 0x8) == 0 { // if not empty + // string "apap" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x70) + o = msgp.AppendBytes(o, (*z).ApprovalProgram) + } + if (zb0004Mask & 0x10) == 0 { // if not empty + // string "apat" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x74) + if (*z).Accounts == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).Accounts))) + } + for zb0002 := range (*z).Accounts { + o, err = (*z).Accounts[zb0002].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Accounts", zb0002) + return + } + } + } + if (zb0004Mask & 0x20) == 0 { // if not empty + // string "apfa" + o = append(o, 0xa4, 0x61, 0x70, 0x66, 0x61) + if (*z).ForeignApps == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ForeignApps))) + } + for zb0003 := range (*z).ForeignApps { + o, err = (*z).ForeignApps[zb0003].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ForeignApps", zb0003) + return + } + } + } + if (zb0004Mask & 0x40) == 0 { // if not empty + // string "apgs" + o = append(o, 0xa4, 0x61, 0x70, 0x67, 0x73) + o, err = (*z).GlobalStateSchema.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + } + if (zb0004Mask & 0x80) == 0 { // if not empty + // string "apid" + o = append(o, 0xa4, 0x61, 0x70, 0x69, 0x64) + o, err = (*z).ApplicationID.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ApplicationID") + return + } + } + if (zb0004Mask & 0x100) == 0 { // if not empty + // string "apls" + o = append(o, 0xa4, 0x61, 0x70, 0x6c, 0x73) + o, err = (*z).LocalStateSchema.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + } + if (zb0004Mask & 0x200) == 0 { // if not empty + // string "apsu" + o = append(o, 0xa4, 0x61, 0x70, 0x73, 0x75) + o = msgp.AppendBytes(o, (*z).ClearStateProgram) + } + } + return +} + +func (_ *ApplicationCallTxnFields) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*ApplicationCallTxnFields) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0004 int + var zb0005 bool + zb0004, zb0005, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0004 > 0 { + zb0004-- + bts, err = (*z).ApplicationID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationID") + return + } + } + if zb0004 > 0 { + zb0004-- + { + var zb0006 uint64 + zb0006, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "OnCompletion") + return + } + (*z).OnCompletion = OnCompletion(zb0006) + } + } + if zb0004 > 0 { + zb0004-- + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") + return + } + if zb0007 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0007), uint64(encodedMaxApplicationArgs)) + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") + return + } + if zb0008 { + (*z).ApplicationArgs = nil + } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0007 { + (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0007] + } else { + (*z).ApplicationArgs = make([][]byte, zb0007) + } + for zb0001 := range (*z).ApplicationArgs { + (*z).ApplicationArgs[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationArgs[zb0001]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs", zb0001) + return + } + } + } + if zb0004 > 0 { + zb0004-- + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Accounts") + return + } + if zb0009 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxAccounts)) + err = msgp.WrapError(err, "struct-from-array", "Accounts") + return + } + if zb0010 { + (*z).Accounts = nil + } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0009 { + (*z).Accounts = ((*z).Accounts)[:zb0009] + } else { + (*z).Accounts = make([]basics.Address, zb0009) + } + for zb0002 := range (*z).Accounts { + bts, err = (*z).Accounts[zb0002].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Accounts", zb0002) + return + } + } + } + if zb0004 > 0 { + zb0004-- + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignApps") + return + } + if zb0011 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxForeignApps)) + err = msgp.WrapError(err, "struct-from-array", "ForeignApps") + return + } + if zb0012 { + (*z).ForeignApps = nil + } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0011 { + (*z).ForeignApps = ((*z).ForeignApps)[:zb0011] + } else { + (*z).ForeignApps = make([]basics.AppIndex, zb0011) + } + for zb0003 := range (*z).ForeignApps { + bts, err = (*z).ForeignApps[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignApps", zb0003) + return + } + } + } + if zb0004 > 0 { + zb0004-- + bts, err = (*z).LocalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + } + if zb0004 > 0 { + zb0004-- + bts, err = (*z).GlobalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + } + if zb0004 > 0 { + zb0004-- + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + } + if zb0004 > 0 { + zb0004-- + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + } + if zb0004 > 0 { + err = msgp.ErrTooManyArrayFields(zb0004) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0005 { + (*z) = ApplicationCallTxnFields{} + } + for zb0004 > 0 { + zb0004-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "apid": + bts, err = (*z).ApplicationID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ApplicationID") + return + } + case "apan": + { + var zb0013 uint64 + zb0013, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "OnCompletion") + return + } + (*z).OnCompletion = OnCompletion(zb0013) + } + case "apaa": + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ApplicationArgs") + return + } + if zb0014 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxApplicationArgs)) + err = msgp.WrapError(err, "ApplicationArgs") + return + } + if zb0015 { + (*z).ApplicationArgs = nil + } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0014 { + (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0014] + } else { + (*z).ApplicationArgs = make([][]byte, zb0014) + } + for zb0001 := range (*z).ApplicationArgs { + (*z).ApplicationArgs[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationArgs[zb0001]) + if err != nil { + err = msgp.WrapError(err, "ApplicationArgs", zb0001) + return + } + } + case "apat": + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Accounts") + return + } + if zb0016 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxAccounts)) + err = msgp.WrapError(err, "Accounts") + return + } + if zb0017 { + (*z).Accounts = nil + } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0016 { + (*z).Accounts = ((*z).Accounts)[:zb0016] + } else { + (*z).Accounts = make([]basics.Address, zb0016) + } + for zb0002 := range (*z).Accounts { + bts, err = (*z).Accounts[zb0002].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Accounts", zb0002) + return + } + } + case "apfa": + var zb0018 int + var zb0019 bool + zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignApps") + return + } + if zb0018 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0018), uint64(encodedMaxForeignApps)) + err = msgp.WrapError(err, "ForeignApps") + return + } + if zb0019 { + (*z).ForeignApps = nil + } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0018 { + (*z).ForeignApps = ((*z).ForeignApps)[:zb0018] + } else { + (*z).ForeignApps = make([]basics.AppIndex, zb0018) + } + for zb0003 := range (*z).ForeignApps { + bts, err = (*z).ForeignApps[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignApps", zb0003) + return + } + } + case "apls": + bts, err = (*z).LocalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + case "apgs": + bts, err = (*z).GlobalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + case "apap": + (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + case "apsu": + (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *ApplicationCallTxnFields) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*ApplicationCallTxnFields) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *ApplicationCallTxnFields) Msgsize() (s int) { + s = 1 + 5 + (*z).ApplicationID.Msgsize() + 5 + msgp.Uint64Size + 5 + msgp.ArrayHeaderSize + for zb0001 := range (*z).ApplicationArgs { + s += msgp.BytesPrefixSize + len((*z).ApplicationArgs[zb0001]) + } + s += 5 + msgp.ArrayHeaderSize + for zb0002 := range (*z).Accounts { + s += (*z).Accounts[zb0002].Msgsize() + } + s += 5 + msgp.ArrayHeaderSize + for zb0003 := range (*z).ForeignApps { + s += (*z).ForeignApps[zb0003].Msgsize() + } + s += 5 + (*z).LocalStateSchema.Msgsize() + 5 + (*z).GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *ApplicationCallTxnFields) MsgIsZero() bool { + return ((*z).ApplicationID.MsgIsZero()) && ((*z).OnCompletion == 0) && (len((*z).ApplicationArgs) == 0) && (len((*z).Accounts) == 0) && (len((*z).ForeignApps) == 0) && ((*z).LocalStateSchema.MsgIsZero()) && ((*z).GlobalStateSchema.MsgIsZero()) && (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) +} + // MarshalMsg implements msgp.Marshaler func (z *ApplyData) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(4) - var zb0001Mask uint8 /* 5 bits */ + zb0001Len := uint32(5) + var zb0001Mask uint8 /* 6 bits */ if (*z).ClosingAmount.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x2 } - if (*z).CloseRewards.MsgIsZero() { + if (*z).EvalDelta.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x4 } - if (*z).ReceiverRewards.MsgIsZero() { + if (*z).CloseRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x8 } - if (*z).SenderRewards.MsgIsZero() { + if (*z).ReceiverRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x10 } + if (*z).SenderRewards.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) if zb0001Len != 0 { @@ -172,6 +670,15 @@ func (z *ApplyData) MarshalMsg(b []byte) (o []byte, err error) { } } if (zb0001Mask & 0x4) == 0 { // if not empty + // string "dt" + o = append(o, 0xa2, 0x64, 0x74) + o, err = (*z).EvalDelta.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } + } + if (zb0001Mask & 0x8) == 0 { // if not empty // string "rc" o = append(o, 0xa2, 0x72, 0x63) o, err = (*z).CloseRewards.MarshalMsg(o) @@ -180,7 +687,7 @@ func (z *ApplyData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x8) == 0 { // if not empty + if (zb0001Mask & 0x10) == 0 { // if not empty // string "rr" o = append(o, 0xa2, 0x72, 0x72) o, err = (*z).ReceiverRewards.MarshalMsg(o) @@ -189,7 +696,7 @@ func (z *ApplyData) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x10) == 0 { // if not empty + if (zb0001Mask & 0x20) == 0 { // if not empty // string "rs" o = append(o, 0xa2, 0x72, 0x73) o, err = (*z).SenderRewards.MarshalMsg(o) @@ -252,6 +759,14 @@ func (z *ApplyData) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "EvalDelta") + return + } + } if zb0001 > 0 { err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { @@ -299,6 +814,12 @@ func (z *ApplyData) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "CloseRewards") return } + case "dt": + bts, err = (*z).EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -319,13 +840,13 @@ func (_ *ApplyData) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *ApplyData) Msgsize() (s int) { - s = 1 + 3 + (*z).ClosingAmount.Msgsize() + 3 + (*z).SenderRewards.Msgsize() + 3 + (*z).ReceiverRewards.Msgsize() + 3 + (*z).CloseRewards.Msgsize() + s = 1 + 3 + (*z).ClosingAmount.Msgsize() + 3 + (*z).SenderRewards.Msgsize() + 3 + (*z).ReceiverRewards.Msgsize() + 3 + (*z).CloseRewards.Msgsize() + 3 + (*z).EvalDelta.Msgsize() return } // MsgIsZero returns whether this is a zero value func (z *ApplyData) MsgIsZero() bool { - return ((*z).ClosingAmount.MsgIsZero()) && ((*z).SenderRewards.MsgIsZero()) && ((*z).ReceiverRewards.MsgIsZero()) && ((*z).CloseRewards.MsgIsZero()) + return ((*z).ClosingAmount.MsgIsZero()) && ((*z).SenderRewards.MsgIsZero()) && ((*z).ReceiverRewards.MsgIsZero()) && ((*z).CloseRewards.MsgIsZero()) && ((*z).EvalDelta.MsgIsZero()) } // MarshalMsg implements msgp.Marshaler @@ -1699,18 +2220,64 @@ func (z MinFeeError) MsgIsZero() bool { } // MarshalMsg implements msgp.Marshaler -func (z *PaymentTxnFields) MarshalMsg(b []byte) (o []byte, err error) { +func (z OnCompletion) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) - // omitempty: check for empty values - zb0001Len := uint32(3) - var zb0001Mask uint8 /* 4 bits */ - if (*z).Amount.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).CloseRemainderTo.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x4 + o = msgp.AppendUint64(o, uint64(z)) + return +} + +func (_ OnCompletion) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(OnCompletion) + if !ok { + _, ok = (z).(*OnCompletion) + } + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *OnCompletion) UnmarshalMsg(bts []byte) (o []byte, err error) { + { + var zb0001 uint64 + zb0001, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + (*z) = OnCompletion(zb0001) + } + o = bts + return +} + +func (_ *OnCompletion) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*OnCompletion) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z OnCompletion) Msgsize() (s int) { + s = msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z OnCompletion) MsgIsZero() bool { + return z == 0 +} + +// MarshalMsg implements msgp.Marshaler +func (z *PaymentTxnFields) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(3) + var zb0001Mask uint8 /* 4 bits */ + if (*z).Amount.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 + } + if (*z).CloseRemainderTo.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x4 } if (*z).Receiver.MsgIsZero() { zb0001Len-- @@ -2161,52 +2728,56 @@ func (z *SignedTxn) MsgIsZero() bool { func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(11) - var zb0001Mask uint16 /* 15 bits */ + zb0001Len := uint32(12) + var zb0001Mask uint16 /* 16 bits */ if (*z).SignedTxnWithAD.ApplyData.ClosingAmount.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x10 } - if (*z).HasGenesisHash == false { + if (*z).SignedTxnWithAD.ApplyData.EvalDelta.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x20 } - if (*z).HasGenesisID == false { + if (*z).HasGenesisHash == false { zb0001Len-- zb0001Mask |= 0x40 } - if (*z).SignedTxnWithAD.SignedTxn.Lsig.MsgIsZero() { + if (*z).HasGenesisID == false { zb0001Len-- zb0001Mask |= 0x80 } - if (*z).SignedTxnWithAD.SignedTxn.Msig.MsgIsZero() { + if (*z).SignedTxnWithAD.SignedTxn.Lsig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x100 } - if (*z).SignedTxnWithAD.ApplyData.CloseRewards.MsgIsZero() { + if (*z).SignedTxnWithAD.SignedTxn.Msig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x200 } - if (*z).SignedTxnWithAD.ApplyData.ReceiverRewards.MsgIsZero() { + if (*z).SignedTxnWithAD.ApplyData.CloseRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x400 } - if (*z).SignedTxnWithAD.ApplyData.SenderRewards.MsgIsZero() { + if (*z).SignedTxnWithAD.ApplyData.ReceiverRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x800 } - if (*z).SignedTxnWithAD.SignedTxn.AuthAddr.MsgIsZero() { + if (*z).SignedTxnWithAD.ApplyData.SenderRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x1000 } - if (*z).SignedTxnWithAD.SignedTxn.Sig.MsgIsZero() { + if (*z).SignedTxnWithAD.SignedTxn.AuthAddr.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x2000 } - if (*z).SignedTxnWithAD.SignedTxn.Txn.MsgIsZero() { + if (*z).SignedTxnWithAD.SignedTxn.Sig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x4000 } + if (*z).SignedTxnWithAD.SignedTxn.Txn.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x8000 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) if zb0001Len != 0 { @@ -2220,16 +2791,25 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { } } if (zb0001Mask & 0x20) == 0 { // if not empty + // string "dt" + o = append(o, 0xa2, 0x64, 0x74) + o, err = (*z).SignedTxnWithAD.ApplyData.EvalDelta.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } + } + if (zb0001Mask & 0x40) == 0 { // if not empty // string "hgh" o = append(o, 0xa3, 0x68, 0x67, 0x68) o = msgp.AppendBool(o, (*z).HasGenesisHash) } - if (zb0001Mask & 0x40) == 0 { // if not empty + if (zb0001Mask & 0x80) == 0 { // if not empty // string "hgi" o = append(o, 0xa3, 0x68, 0x67, 0x69) o = msgp.AppendBool(o, (*z).HasGenesisID) } - if (zb0001Mask & 0x80) == 0 { // if not empty + if (zb0001Mask & 0x100) == 0 { // if not empty // string "lsig" o = append(o, 0xa4, 0x6c, 0x73, 0x69, 0x67) o, err = (*z).SignedTxnWithAD.SignedTxn.Lsig.MarshalMsg(o) @@ -2238,7 +2818,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x100) == 0 { // if not empty + if (zb0001Mask & 0x200) == 0 { // if not empty // string "msig" o = append(o, 0xa4, 0x6d, 0x73, 0x69, 0x67) o, err = (*z).SignedTxnWithAD.SignedTxn.Msig.MarshalMsg(o) @@ -2247,7 +2827,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x200) == 0 { // if not empty + if (zb0001Mask & 0x400) == 0 { // if not empty // string "rc" o = append(o, 0xa2, 0x72, 0x63) o, err = (*z).SignedTxnWithAD.ApplyData.CloseRewards.MarshalMsg(o) @@ -2256,7 +2836,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x400) == 0 { // if not empty + if (zb0001Mask & 0x800) == 0 { // if not empty // string "rr" o = append(o, 0xa2, 0x72, 0x72) o, err = (*z).SignedTxnWithAD.ApplyData.ReceiverRewards.MarshalMsg(o) @@ -2265,7 +2845,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x800) == 0 { // if not empty + if (zb0001Mask & 0x1000) == 0 { // if not empty // string "rs" o = append(o, 0xa2, 0x72, 0x73) o, err = (*z).SignedTxnWithAD.ApplyData.SenderRewards.MarshalMsg(o) @@ -2274,7 +2854,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x1000) == 0 { // if not empty + if (zb0001Mask & 0x2000) == 0 { // if not empty // string "sgnr" o = append(o, 0xa4, 0x73, 0x67, 0x6e, 0x72) o, err = (*z).SignedTxnWithAD.SignedTxn.AuthAddr.MarshalMsg(o) @@ -2283,7 +2863,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x2000) == 0 { // if not empty + if (zb0001Mask & 0x4000) == 0 { // if not empty // string "sig" o = append(o, 0xa3, 0x73, 0x69, 0x67) o, err = (*z).SignedTxnWithAD.SignedTxn.Sig.MarshalMsg(o) @@ -2292,7 +2872,7 @@ func (z *SignedTxnInBlock) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x4000) == 0 { // if not empty + if (zb0001Mask & 0x8000) == 0 { // if not empty // string "txn" o = append(o, 0xa3, 0x74, 0x78, 0x6e) o, err = (*z).SignedTxnWithAD.SignedTxn.Txn.MarshalMsg(o) @@ -2395,6 +2975,14 @@ func (z *SignedTxnInBlock) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).SignedTxnWithAD.ApplyData.EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "EvalDelta") + return + } + } if zb0001 > 0 { zb0001-- (*z).HasGenesisID, bts, err = msgp.ReadBoolBytes(bts) @@ -2488,6 +3076,12 @@ func (z *SignedTxnInBlock) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "CloseRewards") return } + case "dt": + bts, err = (*z).SignedTxnWithAD.ApplyData.EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } case "hgi": (*z).HasGenesisID, bts, err = msgp.ReadBoolBytes(bts) if err != nil { @@ -2520,57 +3114,61 @@ func (_ *SignedTxnInBlock) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *SignedTxnInBlock) Msgsize() (s int) { - s = 1 + 4 + (*z).SignedTxnWithAD.SignedTxn.Sig.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.Msig.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.Lsig.Msgsize() + 4 + (*z).SignedTxnWithAD.SignedTxn.Txn.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.AuthAddr.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.ClosingAmount.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.SenderRewards.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.ReceiverRewards.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.CloseRewards.Msgsize() + 4 + msgp.BoolSize + 4 + msgp.BoolSize + s = 1 + 4 + (*z).SignedTxnWithAD.SignedTxn.Sig.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.Msig.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.Lsig.Msgsize() + 4 + (*z).SignedTxnWithAD.SignedTxn.Txn.Msgsize() + 5 + (*z).SignedTxnWithAD.SignedTxn.AuthAddr.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.ClosingAmount.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.SenderRewards.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.ReceiverRewards.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.CloseRewards.Msgsize() + 3 + (*z).SignedTxnWithAD.ApplyData.EvalDelta.Msgsize() + 4 + msgp.BoolSize + 4 + msgp.BoolSize return } // MsgIsZero returns whether this is a zero value func (z *SignedTxnInBlock) MsgIsZero() bool { - return ((*z).SignedTxnWithAD.SignedTxn.Sig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Msig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Lsig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Txn.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.AuthAddr.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.ClosingAmount.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.SenderRewards.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.ReceiverRewards.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.CloseRewards.MsgIsZero()) && ((*z).HasGenesisID == false) && ((*z).HasGenesisHash == false) + return ((*z).SignedTxnWithAD.SignedTxn.Sig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Msig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Lsig.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.Txn.MsgIsZero()) && ((*z).SignedTxnWithAD.SignedTxn.AuthAddr.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.ClosingAmount.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.SenderRewards.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.ReceiverRewards.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.CloseRewards.MsgIsZero()) && ((*z).SignedTxnWithAD.ApplyData.EvalDelta.MsgIsZero()) && ((*z).HasGenesisID == false) && ((*z).HasGenesisHash == false) } // MarshalMsg implements msgp.Marshaler func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(9) - var zb0001Mask uint16 /* 12 bits */ + zb0001Len := uint32(10) + var zb0001Mask uint16 /* 13 bits */ if (*z).ApplyData.ClosingAmount.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x8 } - if (*z).SignedTxn.Lsig.MsgIsZero() { + if (*z).ApplyData.EvalDelta.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x10 } - if (*z).SignedTxn.Msig.MsgIsZero() { + if (*z).SignedTxn.Lsig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x20 } - if (*z).ApplyData.CloseRewards.MsgIsZero() { + if (*z).SignedTxn.Msig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x40 } - if (*z).ApplyData.ReceiverRewards.MsgIsZero() { + if (*z).ApplyData.CloseRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x80 } - if (*z).ApplyData.SenderRewards.MsgIsZero() { + if (*z).ApplyData.ReceiverRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x100 } - if (*z).SignedTxn.AuthAddr.MsgIsZero() { + if (*z).ApplyData.SenderRewards.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x200 } - if (*z).SignedTxn.Sig.MsgIsZero() { + if (*z).SignedTxn.AuthAddr.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x400 } - if (*z).SignedTxn.Txn.MsgIsZero() { + if (*z).SignedTxn.Sig.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x800 } + if (*z).SignedTxn.Txn.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x1000 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) if zb0001Len != 0 { @@ -2584,6 +3182,15 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { } } if (zb0001Mask & 0x10) == 0 { // if not empty + // string "dt" + o = append(o, 0xa2, 0x64, 0x74) + o, err = (*z).ApplyData.EvalDelta.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } + } + if (zb0001Mask & 0x20) == 0 { // if not empty // string "lsig" o = append(o, 0xa4, 0x6c, 0x73, 0x69, 0x67) o, err = (*z).SignedTxn.Lsig.MarshalMsg(o) @@ -2592,7 +3199,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x20) == 0 { // if not empty + if (zb0001Mask & 0x40) == 0 { // if not empty // string "msig" o = append(o, 0xa4, 0x6d, 0x73, 0x69, 0x67) o, err = (*z).SignedTxn.Msig.MarshalMsg(o) @@ -2601,7 +3208,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x40) == 0 { // if not empty + if (zb0001Mask & 0x80) == 0 { // if not empty // string "rc" o = append(o, 0xa2, 0x72, 0x63) o, err = (*z).ApplyData.CloseRewards.MarshalMsg(o) @@ -2610,7 +3217,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x80) == 0 { // if not empty + if (zb0001Mask & 0x100) == 0 { // if not empty // string "rr" o = append(o, 0xa2, 0x72, 0x72) o, err = (*z).ApplyData.ReceiverRewards.MarshalMsg(o) @@ -2619,7 +3226,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x100) == 0 { // if not empty + if (zb0001Mask & 0x200) == 0 { // if not empty // string "rs" o = append(o, 0xa2, 0x72, 0x73) o, err = (*z).ApplyData.SenderRewards.MarshalMsg(o) @@ -2628,7 +3235,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x200) == 0 { // if not empty + if (zb0001Mask & 0x400) == 0 { // if not empty // string "sgnr" o = append(o, 0xa4, 0x73, 0x67, 0x6e, 0x72) o, err = (*z).SignedTxn.AuthAddr.MarshalMsg(o) @@ -2637,7 +3244,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x400) == 0 { // if not empty + if (zb0001Mask & 0x800) == 0 { // if not empty // string "sig" o = append(o, 0xa3, 0x73, 0x69, 0x67) o, err = (*z).SignedTxn.Sig.MarshalMsg(o) @@ -2646,7 +3253,7 @@ func (z *SignedTxnWithAD) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0001Mask & 0x800) == 0 { // if not empty + if (zb0001Mask & 0x1000) == 0 { // if not empty // string "txn" o = append(o, 0xa3, 0x74, 0x78, 0x6e) o, err = (*z).SignedTxn.Txn.MarshalMsg(o) @@ -2749,6 +3356,14 @@ func (z *SignedTxnWithAD) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).ApplyData.EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "EvalDelta") + return + } + } if zb0001 > 0 { err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { @@ -2826,6 +3441,12 @@ func (z *SignedTxnWithAD) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "CloseRewards") return } + case "dt": + bts, err = (*z).ApplyData.EvalDelta.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "EvalDelta") + return + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -2846,150 +3467,186 @@ func (_ *SignedTxnWithAD) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *SignedTxnWithAD) Msgsize() (s int) { - s = 1 + 4 + (*z).SignedTxn.Sig.Msgsize() + 5 + (*z).SignedTxn.Msig.Msgsize() + 5 + (*z).SignedTxn.Lsig.Msgsize() + 4 + (*z).SignedTxn.Txn.Msgsize() + 5 + (*z).SignedTxn.AuthAddr.Msgsize() + 3 + (*z).ApplyData.ClosingAmount.Msgsize() + 3 + (*z).ApplyData.SenderRewards.Msgsize() + 3 + (*z).ApplyData.ReceiverRewards.Msgsize() + 3 + (*z).ApplyData.CloseRewards.Msgsize() + s = 1 + 4 + (*z).SignedTxn.Sig.Msgsize() + 5 + (*z).SignedTxn.Msig.Msgsize() + 5 + (*z).SignedTxn.Lsig.Msgsize() + 4 + (*z).SignedTxn.Txn.Msgsize() + 5 + (*z).SignedTxn.AuthAddr.Msgsize() + 3 + (*z).ApplyData.ClosingAmount.Msgsize() + 3 + (*z).ApplyData.SenderRewards.Msgsize() + 3 + (*z).ApplyData.ReceiverRewards.Msgsize() + 3 + (*z).ApplyData.CloseRewards.Msgsize() + 3 + (*z).ApplyData.EvalDelta.Msgsize() return } // MsgIsZero returns whether this is a zero value func (z *SignedTxnWithAD) MsgIsZero() bool { - return ((*z).SignedTxn.Sig.MsgIsZero()) && ((*z).SignedTxn.Msig.MsgIsZero()) && ((*z).SignedTxn.Lsig.MsgIsZero()) && ((*z).SignedTxn.Txn.MsgIsZero()) && ((*z).SignedTxn.AuthAddr.MsgIsZero()) && ((*z).ApplyData.ClosingAmount.MsgIsZero()) && ((*z).ApplyData.SenderRewards.MsgIsZero()) && ((*z).ApplyData.ReceiverRewards.MsgIsZero()) && ((*z).ApplyData.CloseRewards.MsgIsZero()) + return ((*z).SignedTxn.Sig.MsgIsZero()) && ((*z).SignedTxn.Msig.MsgIsZero()) && ((*z).SignedTxn.Lsig.MsgIsZero()) && ((*z).SignedTxn.Txn.MsgIsZero()) && ((*z).SignedTxn.AuthAddr.MsgIsZero()) && ((*z).ApplyData.ClosingAmount.MsgIsZero()) && ((*z).ApplyData.SenderRewards.MsgIsZero()) && ((*z).ApplyData.ReceiverRewards.MsgIsZero()) && ((*z).ApplyData.CloseRewards.MsgIsZero()) && ((*z).ApplyData.EvalDelta.MsgIsZero()) } // MarshalMsg implements msgp.Marshaler func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0002Len := uint32(30) - var zb0002Mask uint64 /* 37 bits */ + zb0005Len := uint32(39) + var zb0005Mask uint64 /* 47 bits */ if (*z).AssetTransferTxnFields.AssetAmount == 0 { - zb0002Len-- - zb0002Mask |= 0x80 + zb0005Len-- + zb0005Mask |= 0x100 } if (*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x100 + zb0005Len-- + zb0005Mask |= 0x200 } if (*z).AssetFreezeTxnFields.AssetFrozen == false { - zb0002Len-- - zb0002Mask |= 0x200 + zb0005Len-- + zb0005Mask |= 0x400 } if (*z).PaymentTxnFields.Amount.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x400 + zb0005Len-- + zb0005Mask |= 0x800 + } + if len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0 { + zb0005Len-- + zb0005Mask |= 0x1000 + } + if (*z).ApplicationCallTxnFields.OnCompletion == 0 { + zb0005Len-- + zb0005Mask |= 0x2000 + } + if len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0 { + zb0005Len-- + zb0005Mask |= 0x4000 } if (*z).AssetConfigTxnFields.AssetParams.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x800 + zb0005Len-- + zb0005Mask |= 0x8000 + } + if len((*z).ApplicationCallTxnFields.Accounts) == 0 { + zb0005Len-- + zb0005Mask |= 0x10000 + } + if len((*z).ApplicationCallTxnFields.ForeignApps) == 0 { + zb0005Len-- + zb0005Mask |= 0x20000 + } + if (*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero() { + zb0005Len-- + zb0005Mask |= 0x40000 + } + if (*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero() { + zb0005Len-- + zb0005Mask |= 0x80000 + } + if (*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero() { + zb0005Len-- + zb0005Mask |= 0x100000 + } + if len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0 { + zb0005Len-- + zb0005Mask |= 0x200000 } if (*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x1000 + zb0005Len-- + zb0005Mask |= 0x400000 } if (*z).AssetTransferTxnFields.AssetSender.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x2000 + zb0005Len-- + zb0005Mask |= 0x800000 } if (*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x4000 + zb0005Len-- + zb0005Mask |= 0x1000000 } if (*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x8000 + zb0005Len-- + zb0005Mask |= 0x2000000 } if (*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x10000 + zb0005Len-- + zb0005Mask |= 0x4000000 } if (*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x20000 + zb0005Len-- + zb0005Mask |= 0x8000000 } if (*z).Header.Fee.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x40000 + zb0005Len-- + zb0005Mask |= 0x10000000 } if (*z).Header.FirstValid.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x80000 + zb0005Len-- + zb0005Mask |= 0x20000000 } if (*z).Header.GenesisID == "" { - zb0002Len-- - zb0002Mask |= 0x100000 + zb0005Len-- + zb0005Mask |= 0x40000000 } if (*z).Header.GenesisHash.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x200000 + zb0005Len-- + zb0005Mask |= 0x80000000 } if (*z).Header.Group.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x400000 + zb0005Len-- + zb0005Mask |= 0x100000000 } if (*z).Header.LastValid.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x800000 + zb0005Len-- + zb0005Mask |= 0x200000000 } if (*z).Header.Lease == ([32]byte{}) { - zb0002Len-- - zb0002Mask |= 0x1000000 + zb0005Len-- + zb0005Mask |= 0x400000000 } if (*z).KeyregTxnFields.Nonparticipation == false { - zb0002Len-- - zb0002Mask |= 0x2000000 + zb0005Len-- + zb0005Mask |= 0x800000000 } if len((*z).Header.Note) == 0 { - zb0002Len-- - zb0002Mask |= 0x4000000 + zb0005Len-- + zb0005Mask |= 0x1000000000 } if (*z).PaymentTxnFields.Receiver.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x8000000 + zb0005Len-- + zb0005Mask |= 0x2000000000 } if (*z).Header.RekeyTo.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x10000000 + zb0005Len-- + zb0005Mask |= 0x4000000000 } if (*z).KeyregTxnFields.SelectionPK.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x20000000 + zb0005Len-- + zb0005Mask |= 0x8000000000 } if (*z).Header.Sender.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x40000000 + zb0005Len-- + zb0005Mask |= 0x10000000000 } if (*z).Type.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x80000000 + zb0005Len-- + zb0005Mask |= 0x20000000000 } if (*z).KeyregTxnFields.VoteFirst.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x100000000 + zb0005Len-- + zb0005Mask |= 0x40000000000 } if (*z).KeyregTxnFields.VoteKeyDilution == 0 { - zb0002Len-- - zb0002Mask |= 0x200000000 + zb0005Len-- + zb0005Mask |= 0x80000000000 } if (*z).KeyregTxnFields.VotePK.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x400000000 + zb0005Len-- + zb0005Mask |= 0x100000000000 } if (*z).KeyregTxnFields.VoteLast.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x800000000 + zb0005Len-- + zb0005Mask |= 0x200000000000 } if (*z).AssetTransferTxnFields.XferAsset.MsgIsZero() { - zb0002Len-- - zb0002Mask |= 0x1000000000 + zb0005Len-- + zb0005Mask |= 0x400000000000 } - // variable map header, size zb0002Len - o = msgp.AppendMapHeader(o, zb0002Len) - if zb0002Len != 0 { - if (zb0002Mask & 0x80) == 0 { // if not empty + // variable map header, size zb0005Len + o = msgp.AppendMapHeader(o, zb0005Len) + if zb0005Len != 0 { + if (zb0005Mask & 0x100) == 0 { // if not empty // string "aamt" o = append(o, 0xa4, 0x61, 0x61, 0x6d, 0x74) o = msgp.AppendUint64(o, (*z).AssetTransferTxnFields.AssetAmount) } - if (zb0002Mask & 0x100) == 0 { // if not empty + if (zb0005Mask & 0x200) == 0 { // if not empty // string "aclose" o = append(o, 0xa6, 0x61, 0x63, 0x6c, 0x6f, 0x73, 0x65) o, err = (*z).AssetTransferTxnFields.AssetCloseTo.MarshalMsg(o) @@ -2998,12 +3655,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x200) == 0 { // if not empty + if (zb0005Mask & 0x400) == 0 { // if not empty // string "afrz" o = append(o, 0xa4, 0x61, 0x66, 0x72, 0x7a) o = msgp.AppendBool(o, (*z).AssetFreezeTxnFields.AssetFrozen) } - if (zb0002Mask & 0x400) == 0 { // if not empty + if (zb0005Mask & 0x800) == 0 { // if not empty // string "amt" o = append(o, 0xa3, 0x61, 0x6d, 0x74) o, err = (*z).PaymentTxnFields.Amount.MarshalMsg(o) @@ -3012,7 +3669,29 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x800) == 0 { // if not empty + if (zb0005Mask & 0x1000) == 0 { // if not empty + // string "apaa" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x61) + if (*z).ApplicationCallTxnFields.ApplicationArgs == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ApplicationCallTxnFields.ApplicationArgs))) + } + for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { + o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) + } + } + if (zb0005Mask & 0x2000) == 0 { // if not empty + // string "apan" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x6e) + o = msgp.AppendUint64(o, uint64((*z).ApplicationCallTxnFields.OnCompletion)) + } + if (zb0005Mask & 0x4000) == 0 { // if not empty + // string "apap" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x70) + o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ApprovalProgram) + } + if (zb0005Mask & 0x8000) == 0 { // if not empty // string "apar" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x72) o, err = (*z).AssetConfigTxnFields.AssetParams.MarshalMsg(o) @@ -3021,7 +3700,71 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x1000) == 0 { // if not empty + if (zb0005Mask & 0x10000) == 0 { // if not empty + // string "apat" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x74) + if (*z).ApplicationCallTxnFields.Accounts == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ApplicationCallTxnFields.Accounts))) + } + for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { + o, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Accounts", zb0003) + return + } + } + } + if (zb0005Mask & 0x20000) == 0 { // if not empty + // string "apfa" + o = append(o, 0xa4, 0x61, 0x70, 0x66, 0x61) + if (*z).ApplicationCallTxnFields.ForeignApps == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ApplicationCallTxnFields.ForeignApps))) + } + for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { + o, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ForeignApps", zb0004) + return + } + } + } + if (zb0005Mask & 0x40000) == 0 { // if not empty + // string "apgs" + o = append(o, 0xa4, 0x61, 0x70, 0x67, 0x73) + o, err = (*z).ApplicationCallTxnFields.GlobalStateSchema.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + } + if (zb0005Mask & 0x80000) == 0 { // if not empty + // string "apid" + o = append(o, 0xa4, 0x61, 0x70, 0x69, 0x64) + o, err = (*z).ApplicationCallTxnFields.ApplicationID.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ApplicationID") + return + } + } + if (zb0005Mask & 0x100000) == 0 { // if not empty + // string "apls" + o = append(o, 0xa4, 0x61, 0x70, 0x6c, 0x73) + o, err = (*z).ApplicationCallTxnFields.LocalStateSchema.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + } + if (zb0005Mask & 0x200000) == 0 { // if not empty + // string "apsu" + o = append(o, 0xa4, 0x61, 0x70, 0x73, 0x75) + o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ClearStateProgram) + } + if (zb0005Mask & 0x400000) == 0 { // if not empty // string "arcv" o = append(o, 0xa4, 0x61, 0x72, 0x63, 0x76) o, err = (*z).AssetTransferTxnFields.AssetReceiver.MarshalMsg(o) @@ -3030,7 +3773,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x2000) == 0 { // if not empty + if (zb0005Mask & 0x800000) == 0 { // if not empty // string "asnd" o = append(o, 0xa4, 0x61, 0x73, 0x6e, 0x64) o, err = (*z).AssetTransferTxnFields.AssetSender.MarshalMsg(o) @@ -3039,7 +3782,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x4000) == 0 { // if not empty + if (zb0005Mask & 0x1000000) == 0 { // if not empty // string "caid" o = append(o, 0xa4, 0x63, 0x61, 0x69, 0x64) o, err = (*z).AssetConfigTxnFields.ConfigAsset.MarshalMsg(o) @@ -3048,7 +3791,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x8000) == 0 { // if not empty + if (zb0005Mask & 0x2000000) == 0 { // if not empty // string "close" o = append(o, 0xa5, 0x63, 0x6c, 0x6f, 0x73, 0x65) o, err = (*z).PaymentTxnFields.CloseRemainderTo.MarshalMsg(o) @@ -3057,7 +3800,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x10000) == 0 { // if not empty + if (zb0005Mask & 0x4000000) == 0 { // if not empty // string "fadd" o = append(o, 0xa4, 0x66, 0x61, 0x64, 0x64) o, err = (*z).AssetFreezeTxnFields.FreezeAccount.MarshalMsg(o) @@ -3066,7 +3809,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x20000) == 0 { // if not empty + if (zb0005Mask & 0x8000000) == 0 { // if not empty // string "faid" o = append(o, 0xa4, 0x66, 0x61, 0x69, 0x64) o, err = (*z).AssetFreezeTxnFields.FreezeAsset.MarshalMsg(o) @@ -3075,7 +3818,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x40000) == 0 { // if not empty + if (zb0005Mask & 0x10000000) == 0 { // if not empty // string "fee" o = append(o, 0xa3, 0x66, 0x65, 0x65) o, err = (*z).Header.Fee.MarshalMsg(o) @@ -3084,7 +3827,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x80000) == 0 { // if not empty + if (zb0005Mask & 0x20000000) == 0 { // if not empty // string "fv" o = append(o, 0xa2, 0x66, 0x76) o, err = (*z).Header.FirstValid.MarshalMsg(o) @@ -3093,12 +3836,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x100000) == 0 { // if not empty + if (zb0005Mask & 0x40000000) == 0 { // if not empty // string "gen" o = append(o, 0xa3, 0x67, 0x65, 0x6e) o = msgp.AppendString(o, (*z).Header.GenesisID) } - if (zb0002Mask & 0x200000) == 0 { // if not empty + if (zb0005Mask & 0x80000000) == 0 { // if not empty // string "gh" o = append(o, 0xa2, 0x67, 0x68) o, err = (*z).Header.GenesisHash.MarshalMsg(o) @@ -3107,7 +3850,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x400000) == 0 { // if not empty + if (zb0005Mask & 0x100000000) == 0 { // if not empty // string "grp" o = append(o, 0xa3, 0x67, 0x72, 0x70) o, err = (*z).Header.Group.MarshalMsg(o) @@ -3116,7 +3859,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x800000) == 0 { // if not empty + if (zb0005Mask & 0x200000000) == 0 { // if not empty // string "lv" o = append(o, 0xa2, 0x6c, 0x76) o, err = (*z).Header.LastValid.MarshalMsg(o) @@ -3125,22 +3868,22 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x1000000) == 0 { // if not empty + if (zb0005Mask & 0x400000000) == 0 { // if not empty // string "lx" o = append(o, 0xa2, 0x6c, 0x78) o = msgp.AppendBytes(o, ((*z).Header.Lease)[:]) } - if (zb0002Mask & 0x2000000) == 0 { // if not empty + if (zb0005Mask & 0x800000000) == 0 { // if not empty // string "nonpart" o = append(o, 0xa7, 0x6e, 0x6f, 0x6e, 0x70, 0x61, 0x72, 0x74) o = msgp.AppendBool(o, (*z).KeyregTxnFields.Nonparticipation) } - if (zb0002Mask & 0x4000000) == 0 { // if not empty + if (zb0005Mask & 0x1000000000) == 0 { // if not empty // string "note" o = append(o, 0xa4, 0x6e, 0x6f, 0x74, 0x65) o = msgp.AppendBytes(o, (*z).Header.Note) } - if (zb0002Mask & 0x8000000) == 0 { // if not empty + if (zb0005Mask & 0x2000000000) == 0 { // if not empty // string "rcv" o = append(o, 0xa3, 0x72, 0x63, 0x76) o, err = (*z).PaymentTxnFields.Receiver.MarshalMsg(o) @@ -3149,7 +3892,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x10000000) == 0 { // if not empty + if (zb0005Mask & 0x4000000000) == 0 { // if not empty // string "rekey" o = append(o, 0xa5, 0x72, 0x65, 0x6b, 0x65, 0x79) o, err = (*z).Header.RekeyTo.MarshalMsg(o) @@ -3158,7 +3901,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x20000000) == 0 { // if not empty + if (zb0005Mask & 0x8000000000) == 0 { // if not empty // string "selkey" o = append(o, 0xa6, 0x73, 0x65, 0x6c, 0x6b, 0x65, 0x79) o, err = (*z).KeyregTxnFields.SelectionPK.MarshalMsg(o) @@ -3167,7 +3910,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x40000000) == 0 { // if not empty + if (zb0005Mask & 0x10000000000) == 0 { // if not empty // string "snd" o = append(o, 0xa3, 0x73, 0x6e, 0x64) o, err = (*z).Header.Sender.MarshalMsg(o) @@ -3176,7 +3919,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x80000000) == 0 { // if not empty + if (zb0005Mask & 0x20000000000) == 0 { // if not empty // string "type" o = append(o, 0xa4, 0x74, 0x79, 0x70, 0x65) o, err = (*z).Type.MarshalMsg(o) @@ -3185,7 +3928,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x100000000) == 0 { // if not empty + if (zb0005Mask & 0x40000000000) == 0 { // if not empty // string "votefst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x66, 0x73, 0x74) o, err = (*z).KeyregTxnFields.VoteFirst.MarshalMsg(o) @@ -3194,12 +3937,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x200000000) == 0 { // if not empty + if (zb0005Mask & 0x80000000000) == 0 { // if not empty // string "votekd" o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x64) o = msgp.AppendUint64(o, (*z).KeyregTxnFields.VoteKeyDilution) } - if (zb0002Mask & 0x400000000) == 0 { // if not empty + if (zb0005Mask & 0x100000000000) == 0 { // if not empty // string "votekey" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x65, 0x79) o, err = (*z).KeyregTxnFields.VotePK.MarshalMsg(o) @@ -3208,7 +3951,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x800000000) == 0 { // if not empty + if (zb0005Mask & 0x200000000000) == 0 { // if not empty // string "votelst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6c, 0x73, 0x74) o, err = (*z).KeyregTxnFields.VoteLast.MarshalMsg(o) @@ -3217,7 +3960,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0002Mask & 0x1000000000) == 0 { // if not empty + if (zb0005Mask & 0x400000000000) == 0 { // if not empty // string "xaid" o = append(o, 0xa4, 0x78, 0x61, 0x69, 0x64) o, err = (*z).AssetTransferTxnFields.XferAsset.MarshalMsg(o) @@ -3239,257 +3982,396 @@ func (_ *Transaction) CanMarshalMsg(z interface{}) bool { func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0002 int - var zb0003 bool - zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Type.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Type") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.Sender.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Sender") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.Fee.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Fee") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.FirstValid.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FirstValid") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.LastValid.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LastValid") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Note") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).Header.GenesisID, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GenesisID") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.GenesisHash.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GenesisHash") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.Group.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Group") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = msgp.ReadExactBytes(bts, ((*z).Header.Lease)[:]) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Lease") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).Header.RekeyTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "RekeyTo") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).KeyregTxnFields.VotePK.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VotePK") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).KeyregTxnFields.SelectionPK.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "SelectionPK") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).KeyregTxnFields.VoteFirst.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteFirst") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).KeyregTxnFields.VoteLast.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteLast") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).KeyregTxnFields.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).KeyregTxnFields.Nonparticipation, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Nonparticipation") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).PaymentTxnFields.Receiver.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Receiver") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).PaymentTxnFields.Amount.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Amount") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).PaymentTxnFields.CloseRemainderTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "CloseRemainderTo") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetConfigTxnFields.ConfigAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ConfigAsset") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetConfigTxnFields.AssetParams.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetParams") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetTransferTxnFields.XferAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "XferAsset") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).AssetTransferTxnFields.AssetAmount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetAmount") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetTransferTxnFields.AssetSender.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetSender") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetTransferTxnFields.AssetReceiver.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetReceiver") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetTransferTxnFields.AssetCloseTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetCloseTo") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetFreezeTxnFields.FreezeAccount.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FreezeAccount") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).AssetFreezeTxnFields.FreezeAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FreezeAsset") return } } - if zb0002 > 0 { - zb0002-- + if zb0005 > 0 { + zb0005-- (*z).AssetFreezeTxnFields.AssetFrozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetFrozen") return } } - if zb0002 > 0 { - err = msgp.ErrTooManyArrayFields(zb0002) + if zb0005 > 0 { + zb0005-- + bts, err = (*z).ApplicationCallTxnFields.ApplicationID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationID") + return + } + } + if zb0005 > 0 { + zb0005-- + { + var zb0007 uint64 + zb0007, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "OnCompletion") + return + } + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0007) + } + } + if zb0005 > 0 { + zb0005-- + var zb0008 int + var zb0009 bool + zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") + return + } + if zb0008 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0008), uint64(encodedMaxApplicationArgs)) + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") + return + } + if zb0009 { + (*z).ApplicationCallTxnFields.ApplicationArgs = nil + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0008 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0008] + } else { + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0008) + } + for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { + (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs", zb0002) + return + } + } + } + if zb0005 > 0 { + zb0005-- + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Accounts") + return + } + if zb0010 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxAccounts)) + err = msgp.WrapError(err, "struct-from-array", "Accounts") + return + } + if zb0011 { + (*z).ApplicationCallTxnFields.Accounts = nil + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0010 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0010] + } else { + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0010) + } + for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { + bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Accounts", zb0003) + return + } + } + } + if zb0005 > 0 { + zb0005-- + var zb0012 int + var zb0013 bool + zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignApps") + return + } + if zb0012 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxForeignApps)) + err = msgp.WrapError(err, "struct-from-array", "ForeignApps") + return + } + if zb0013 { + (*z).ApplicationCallTxnFields.ForeignApps = nil + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0012 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0012] + } else { + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0012) + } + for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { + bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignApps", zb0004) + return + } + } + } + if zb0005 > 0 { + zb0005-- + bts, err = (*z).ApplicationCallTxnFields.LocalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + } + if zb0005 > 0 { + zb0005-- + bts, err = (*z).ApplicationCallTxnFields.GlobalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + } + if zb0005 > 0 { + zb0005-- + (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + } + if zb0005 > 0 { + zb0005-- + (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + } + if zb0005 > 0 { + err = msgp.ErrTooManyArrayFields(zb0005) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -3500,11 +4382,11 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0003 { + if zb0006 { (*z) = Transaction{} } - for zb0002 > 0 { - zb0002-- + for zb0005 > 0 { + zb0005-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) @@ -3691,6 +4573,127 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "AssetFrozen") return } + case "apid": + bts, err = (*z).ApplicationCallTxnFields.ApplicationID.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ApplicationID") + return + } + case "apan": + { + var zb0014 uint64 + zb0014, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "OnCompletion") + return + } + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0014) + } + case "apaa": + var zb0015 int + var zb0016 bool + zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ApplicationArgs") + return + } + if zb0015 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0015), uint64(encodedMaxApplicationArgs)) + err = msgp.WrapError(err, "ApplicationArgs") + return + } + if zb0016 { + (*z).ApplicationCallTxnFields.ApplicationArgs = nil + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0015 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0015] + } else { + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0015) + } + for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { + (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) + if err != nil { + err = msgp.WrapError(err, "ApplicationArgs", zb0002) + return + } + } + case "apat": + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Accounts") + return + } + if zb0017 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0017), uint64(encodedMaxAccounts)) + err = msgp.WrapError(err, "Accounts") + return + } + if zb0018 { + (*z).ApplicationCallTxnFields.Accounts = nil + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0017 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0017] + } else { + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0017) + } + for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { + bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Accounts", zb0003) + return + } + } + case "apfa": + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignApps") + return + } + if zb0019 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxForeignApps)) + err = msgp.WrapError(err, "ForeignApps") + return + } + if zb0020 { + (*z).ApplicationCallTxnFields.ForeignApps = nil + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0019 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0019] + } else { + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0019) + } + for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { + bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignApps", zb0004) + return + } + } + case "apls": + bts, err = (*z).ApplicationCallTxnFields.LocalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + case "apgs": + bts, err = (*z).ApplicationCallTxnFields.GlobalStateSchema.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + case "apap": + (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + case "apsu": + (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -3711,13 +4714,25 @@ func (_ *Transaction) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *Transaction) Msgsize() (s int) { - s = 3 + 5 + (*z).Type.Msgsize() + 4 + (*z).Header.Sender.Msgsize() + 4 + (*z).Header.Fee.Msgsize() + 3 + (*z).Header.FirstValid.Msgsize() + 3 + (*z).Header.LastValid.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).Header.Note) + 4 + msgp.StringPrefixSize + len((*z).Header.GenesisID) + 3 + (*z).Header.GenesisHash.Msgsize() + 4 + (*z).Header.Group.Msgsize() + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 6 + (*z).Header.RekeyTo.Msgsize() + 8 + (*z).KeyregTxnFields.VotePK.Msgsize() + 7 + (*z).KeyregTxnFields.SelectionPK.Msgsize() + 8 + (*z).KeyregTxnFields.VoteFirst.Msgsize() + 8 + (*z).KeyregTxnFields.VoteLast.Msgsize() + 7 + msgp.Uint64Size + 8 + msgp.BoolSize + 4 + (*z).PaymentTxnFields.Receiver.Msgsize() + 4 + (*z).PaymentTxnFields.Amount.Msgsize() + 6 + (*z).PaymentTxnFields.CloseRemainderTo.Msgsize() + 5 + (*z).AssetConfigTxnFields.ConfigAsset.Msgsize() + 5 + (*z).AssetConfigTxnFields.AssetParams.Msgsize() + 5 + (*z).AssetTransferTxnFields.XferAsset.Msgsize() + 5 + msgp.Uint64Size + 5 + (*z).AssetTransferTxnFields.AssetSender.Msgsize() + 5 + (*z).AssetTransferTxnFields.AssetReceiver.Msgsize() + 7 + (*z).AssetTransferTxnFields.AssetCloseTo.Msgsize() + 5 + (*z).AssetFreezeTxnFields.FreezeAccount.Msgsize() + 5 + (*z).AssetFreezeTxnFields.FreezeAsset.Msgsize() + 5 + msgp.BoolSize + s = 3 + 5 + (*z).Type.Msgsize() + 4 + (*z).Header.Sender.Msgsize() + 4 + (*z).Header.Fee.Msgsize() + 3 + (*z).Header.FirstValid.Msgsize() + 3 + (*z).Header.LastValid.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).Header.Note) + 4 + msgp.StringPrefixSize + len((*z).Header.GenesisID) + 3 + (*z).Header.GenesisHash.Msgsize() + 4 + (*z).Header.Group.Msgsize() + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 6 + (*z).Header.RekeyTo.Msgsize() + 8 + (*z).KeyregTxnFields.VotePK.Msgsize() + 7 + (*z).KeyregTxnFields.SelectionPK.Msgsize() + 8 + (*z).KeyregTxnFields.VoteFirst.Msgsize() + 8 + (*z).KeyregTxnFields.VoteLast.Msgsize() + 7 + msgp.Uint64Size + 8 + msgp.BoolSize + 4 + (*z).PaymentTxnFields.Receiver.Msgsize() + 4 + (*z).PaymentTxnFields.Amount.Msgsize() + 6 + (*z).PaymentTxnFields.CloseRemainderTo.Msgsize() + 5 + (*z).AssetConfigTxnFields.ConfigAsset.Msgsize() + 5 + (*z).AssetConfigTxnFields.AssetParams.Msgsize() + 5 + (*z).AssetTransferTxnFields.XferAsset.Msgsize() + 5 + msgp.Uint64Size + 5 + (*z).AssetTransferTxnFields.AssetSender.Msgsize() + 5 + (*z).AssetTransferTxnFields.AssetReceiver.Msgsize() + 7 + (*z).AssetTransferTxnFields.AssetCloseTo.Msgsize() + 5 + (*z).AssetFreezeTxnFields.FreezeAccount.Msgsize() + 5 + (*z).AssetFreezeTxnFields.FreezeAsset.Msgsize() + 5 + msgp.BoolSize + 5 + (*z).ApplicationCallTxnFields.ApplicationID.Msgsize() + 5 + msgp.Uint64Size + 5 + msgp.ArrayHeaderSize + for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { + s += msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) + } + s += 5 + msgp.ArrayHeaderSize + for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { + s += (*z).ApplicationCallTxnFields.Accounts[zb0003].Msgsize() + } + s += 5 + msgp.ArrayHeaderSize + for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { + s += (*z).ApplicationCallTxnFields.ForeignApps[zb0004].Msgsize() + } + s += 5 + (*z).ApplicationCallTxnFields.LocalStateSchema.Msgsize() + 5 + (*z).ApplicationCallTxnFields.GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ClearStateProgram) return } // MsgIsZero returns whether this is a zero value func (z *Transaction) MsgIsZero() bool { - return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) + return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) && ((*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero()) && ((*z).ApplicationCallTxnFields.OnCompletion == 0) && (len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0) && (len((*z).ApplicationCallTxnFields.Accounts) == 0) && (len((*z).ApplicationCallTxnFields.ForeignApps) == 0) && ((*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero()) && ((*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero()) && (len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0) && (len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0) } // MarshalMsg implements msgp.Marshaler diff --git a/data/transactions/msgp_gen_test.go b/data/transactions/msgp_gen_test.go index 06bed4c0af..380695b1ff 100644 --- a/data/transactions/msgp_gen_test.go +++ b/data/transactions/msgp_gen_test.go @@ -11,6 +11,68 @@ import ( "github.com/algorand/msgp/msgp" ) +func TestMarshalUnmarshalApplicationCallTxnFields(t *testing.T) { + v := ApplicationCallTxnFields{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingApplicationCallTxnFields(t *testing.T) { + protocol.RunEncodingTest(t, &ApplicationCallTxnFields{}) +} + +func BenchmarkMarshalMsgApplicationCallTxnFields(b *testing.B) { + v := ApplicationCallTxnFields{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgApplicationCallTxnFields(b *testing.B) { + v := ApplicationCallTxnFields{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalApplicationCallTxnFields(b *testing.B) { + v := ApplicationCallTxnFields{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + func TestMarshalUnmarshalApplyData(t *testing.T) { v := ApplyData{} bts, err := v.MarshalMsg(nil) diff --git a/data/transactions/payment.go b/data/transactions/payment.go index c4d1d63d2d..6cdaa13f4a 100644 --- a/data/transactions/payment.go +++ b/data/transactions/payment.go @@ -101,6 +101,16 @@ func (payment PaymentTxnFields) apply(header Header, balances Balances, spec Spe return fmt.Errorf("cannot close: %d outstanding created assets", len(rec.AssetParams)) } + // Confirm that there is no application-related state remaining + if len(rec.AppLocalStates) > 0 { + return fmt.Errorf("cannot close: %d outstanding applications opted in. Please opt out or clear them", len(rec.AppLocalStates)) + } + + // Can't have created apps remaining either + if len(rec.AppParams) > 0 { + return fmt.Errorf("cannot close: %d outstanding created applications", len(rec.AppParams)) + } + // Clear out entire account record, to allow the DB to GC it rec.AccountData = basics.AccountData{} err = balances.Put(rec) diff --git a/data/transactions/payment_test.go b/data/transactions/payment_test.go index ec62805d32..374daad486 100644 --- a/data/transactions/payment_test.go +++ b/data/transactions/payment_test.go @@ -76,6 +76,14 @@ type mockBalances struct { protocol.ConsensusVersion } +func (balances mockBalances) Round() basics.Round { + return basics.Round(8675309) +} + +func (balances mockBalances) PutWithCreatables(basics.BalanceRecord, []basics.CreatableLocator, []basics.CreatableLocator) error { + return nil +} + func (balances mockBalances) Get(basics.Address, bool) (basics.BalanceRecord, error) { return basics.BalanceRecord{}, nil } @@ -84,6 +92,10 @@ func (balances mockBalances) GetAssetCreator(assetIdx basics.AssetIndex) (basics return basics.Address{}, nil } +func (balances mockBalances) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { + return basics.Address{}, true, nil +} + func (balances mockBalances) Put(basics.BalanceRecord) error { return nil } @@ -118,7 +130,7 @@ func TestPaymentApply(t *testing.T) { Amount: basics.MicroAlgos{Raw: uint64(50)}, }, } - _, err := tx.Apply(mockBalV0, SpecialAddresses{FeeSink: feeSink}, 0) + _, err := tx.Apply(mockBalV0, nil, SpecialAddresses{FeeSink: feeSink}, 0) require.NoError(t, err) } diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 54fa81236c..4300a46956 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -55,11 +55,18 @@ type Balances interface { // A non-nil error means the lookup is impossible (e.g., if the database doesn't have necessary state anymore) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) + Put(basics.BalanceRecord) error + + // PutWithCreatables is like Put, but should be used when creating or deleting an asset or application. + PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error + // GetAssetCreator gets the address of the account whose balance record // contains the asset params GetAssetCreator(aidx basics.AssetIndex) (basics.Address, error) - Put(basics.BalanceRecord) error + // GetAppCreator gets the address of the account whose balance record + // contains the app params + GetAppCreator(aidx basics.AppIndex) (basics.Address, bool, error) // Move MicroAlgos from one account to another, doing all necessary overflow checking (convenience method) // TODO: Does this need to be part of the balances interface, or can it just be implemented here as a function that calls Put and Get? @@ -70,6 +77,15 @@ type Balances interface { ConsensusParams() config.ConsensusParams } +// StateEvaluator is an interface that provides some Stateful TEAL +// functionality that may be passed through to Apply from ledger, avoiding a +// circular dependency between the logic and transactions packages +type StateEvaluator interface { + Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) + Check(program []byte) (cost int, err error) + InitLedger(balances Balances, acctWhitelist []basics.Address, appGlobalWhitelist []basics.AppIndex, appIdx basics.AppIndex) error +} + // Header captures the fields common to every transaction type. type Header struct { _struct struct{} `codec:",omitempty,omitemptyarray"` @@ -117,6 +133,7 @@ type Transaction struct { AssetConfigTxnFields AssetTransferTxnFields AssetFreezeTxnFields + ApplicationCallTxnFields } // ApplyData contains information about the transaction's execution. @@ -130,6 +147,28 @@ type ApplyData struct { SenderRewards basics.MicroAlgos `codec:"rs"` ReceiverRewards basics.MicroAlgos `codec:"rr"` CloseRewards basics.MicroAlgos `codec:"rc"` + EvalDelta basics.EvalDelta `codec:"dt"` +} + +// Equal returns true if two ApplyDatas are equal, ignoring nilness equality on +// EvalDelta's internal deltas (see EvalDelta.Equal for more information) +func (ad ApplyData) Equal(o ApplyData) bool { + if ad.ClosingAmount != o.ClosingAmount { + return false + } + if ad.SenderRewards != o.SenderRewards { + return false + } + if ad.ReceiverRewards != o.ReceiverRewards { + return false + } + if ad.CloseRewards != o.CloseRewards { + return false + } + if !ad.EvalDelta.Equal(o.EvalDelta) { + return false + } + return true } // TxGroup describes a group of transactions that must appear @@ -273,7 +312,79 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa if !proto.Asset { return fmt.Errorf("asset transaction not supported") } + case protocol.ApplicationCallTx: + if !proto.Application { + return fmt.Errorf("application transaction not supported") + } + + // Ensure requested action is valid + switch tx.OnCompletion { + case NoOpOC: + case OptInOC: + case CloseOutOC: + case ClearStateOC: + case UpdateApplicationOC: + case DeleteApplicationOC: + default: + return fmt.Errorf("invalid application OnCompletion") + } + + // Programs may only be set for creation or update + if tx.ApplicationID != 0 && tx.OnCompletion != UpdateApplicationOC { + if len(tx.ApprovalProgram) != 0 || len(tx.ClearStateProgram) != 0 { + return fmt.Errorf("programs may only be specified during application creation or update") + } + } + + // Schemas may only be set during application creation + if tx.ApplicationID != 0 { + if tx.LocalStateSchema != (basics.StateSchema{}) || + tx.GlobalStateSchema != (basics.StateSchema{}) { + return fmt.Errorf("local and global state schemas are immutable") + } + } + + // Limit total number of arguments + if len(tx.ApplicationArgs) > proto.MaxAppArgs { + return fmt.Errorf("too many application args, max %d", proto.MaxAppArgs) + } + + // Sum up argument lengths + var argSum uint64 + for _, arg := range tx.ApplicationArgs { + argSum = basics.AddSaturate(argSum, uint64(len(arg))) + } + + // Limit total length of all arguments + if argSum > uint64(proto.MaxAppTotalArgLen) { + return fmt.Errorf("application args total length too long, max len %d bytes", proto.MaxAppTotalArgLen) + } + + // Limit number of accounts referred to in a single ApplicationCall + if len(tx.Accounts) > proto.MaxAppTxnAccounts { + return fmt.Errorf("tx.Accounts too long, max number of accounts is %d", proto.MaxAppTxnAccounts) + } + + // Limit number of other app global states referred to + if len(tx.ForeignApps) > proto.MaxAppTxnForeignApps { + return fmt.Errorf("tx.ForeignApps too long, max number of foreign apps is %d", proto.MaxAppTxnForeignApps) + } + if len(tx.ApprovalProgram) > proto.MaxAppProgramLen { + return fmt.Errorf("approval program too long. max len %d bytes", proto.MaxAppProgramLen) + } + + if len(tx.ClearStateProgram) > proto.MaxAppProgramLen { + return fmt.Errorf("clear state program too long. max len %d bytes", proto.MaxAppProgramLen) + } + + if tx.LocalStateSchema.NumEntries() > proto.MaxLocalSchemaEntries { + return fmt.Errorf("tx.LocalStateSchema too large, max number of keys is %d", proto.MaxLocalSchemaEntries) + } + + if tx.GlobalStateSchema.NumEntries() > proto.MaxGlobalSchemaEntries { + return fmt.Errorf("tx.GlobalStateSchema too large, max number of keys is %d", proto.MaxGlobalSchemaEntries) + } default: return fmt.Errorf("unknown tx type %v", tx.Type) } @@ -299,6 +410,10 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa nonZeroFields[protocol.AssetFreezeTx] = true } + if !tx.ApplicationCallTxnFields.Empty() { + nonZeroFields[protocol.ApplicationCallTx] = true + } + for t, nonZero := range nonZeroFields { if nonZero && t != tx.Type { return fmt.Errorf("transaction of type %v has non-zero fields for type %v", tx.Type, t) @@ -424,7 +539,7 @@ func (tx Transaction) EstimateEncodedSize() int { } // Apply changes the balances according to this transaction. -func (tx Transaction) Apply(balances Balances, spec SpecialAddresses, ctr uint64) (ad ApplyData, err error) { +func (tx Transaction) Apply(balances Balances, steva StateEvaluator, spec SpecialAddresses, ctr uint64) (ad ApplyData, err error) { params := balances.ConsensusParams() // move fee to pool @@ -470,6 +585,9 @@ func (tx Transaction) Apply(balances Balances, spec SpecialAddresses, ctr uint64 case protocol.AssetFreezeTx: err = tx.AssetFreezeTxnFields.apply(tx.Header, balances, spec, &ad) + case protocol.ApplicationCallTx: + err = tx.ApplicationCallTxnFields.apply(tx.Header, balances, spec, &ad, ctr, steva) + default: err = fmt.Errorf("Unknown transaction type %v", tx.Type) } diff --git a/ledger/accountdb.go b/ledger/accountdb.go index a692f24366..0c1008c048 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -332,7 +332,7 @@ func accountsDbInit(r db.Queryable, w db.Queryable) (*accountsDbQueries, error) return qs, nil } -func (qs *accountsDbQueries) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.AssetLocator, err error) { +func (qs *accountsDbQueries) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { err = db.Retry(func() error { // Query for assets in range rows, err := qs.listAssetsStmt.Query(maxAssetIdx, maxResults) @@ -341,9 +341,9 @@ func (qs *accountsDbQueries) listAssets(maxAssetIdx basics.AssetIndex, maxResult } defer rows.Close() - // For each row, copy into a new AssetLocator and append to results + // For each row, copy into a new CreatableLocator and append to results var buf []byte - var al basics.AssetLocator + var al basics.CreatableLocator for rows.Next() { err := rows.Scan(&al.Index, &buf) if err != nil { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 854d1b6a93..0a8aca97f9 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -291,7 +291,7 @@ func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewa return au.accountsq.lookup(addr) } -func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.AssetLocator, error) { +func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.CreatableLocator, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() @@ -307,14 +307,14 @@ func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults u sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] }) // Check for assets that haven't been synced to disk yet. - var unsyncedAssets []basics.AssetLocator + var unsyncedAssets []basics.CreatableLocator deletedAssets := make(map[basics.AssetIndex]bool) for _, aidx := range keys { delta := au.assets[aidx] if delta.created { // Created asset that only exists in memory - unsyncedAssets = append(unsyncedAssets, basics.AssetLocator{ - Index: aidx, + unsyncedAssets = append(unsyncedAssets, basics.CreatableLocator{ + Index: basics.CreatableIndex(aidx), Creator: delta.creator, }) } else { @@ -325,7 +325,7 @@ func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults u // Check in-memory created assets, which will always be newer than anything // in the database - var res []basics.AssetLocator + var res []basics.CreatableLocator for _, loc := range unsyncedAssets { if uint64(len(res)) == maxResults { return res, nil @@ -349,7 +349,7 @@ func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults u } // Asset was deleted - if _, ok := deletedAssets[loc.Index]; ok { + if _, ok := deletedAssets[basics.AssetIndex(loc.Index)]; ok { continue } diff --git a/ledger/eval.go b/ledger/eval.go index 25f82a97ed..8d2765b84d 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -81,6 +81,14 @@ func (cs *roundCowState) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Add return cs.getAssetCreator(assetIdx) } +func (cs *roundCowState) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { + return basics.Address{}, false, errors.New("applications not implemented") +} + +func (cs *roundCowState) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { + return errors.New("applications not implemented") +} + // wrappers for roundCowState to satisfy the (current) transactions.Balances interface func (cs *roundCowState) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { acctdata, err := cs.lookup(addr) @@ -564,7 +572,7 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti } // Apply the transaction, updating the cow balances - applyData, err := txn.Txn.Apply(cow, spec, cow.txnCounter()) + applyData, err := txn.Txn.Apply(cow, nil, spec, cow.txnCounter()) if err != nil { return fmt.Errorf("transaction %v: %v", txid, err) } @@ -573,11 +581,11 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti // If we are validating and generating, we have no ApplyData yet. if eval.validate && !eval.generate { if eval.proto.ApplyData { - if ad != applyData { + if !ad.Equal(applyData) { return fmt.Errorf("transaction %v: applyData mismatch: %v != %v", txid, ad, applyData) } } else { - if ad != (transactions.ApplyData{}) { + if !ad.Equal(transactions.ApplyData{}) { return fmt.Errorf("transaction %v: applyData not supported", txid) } } diff --git a/ledger/ledger.go b/ledger/ledger.go index e5bb322fe4..481fc9145c 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -340,7 +340,7 @@ func (l *Ledger) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, er // ListAssets takes a maximum asset index and maximum result length, and // returns up to that many asset AssetIDs from the database where asset id is // less than or equal to the maximum. -func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.AssetLocator, err error) { +func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() return l.accts.listAssets(maxAssetIdx, maxResults) diff --git a/logging/logspec/agreementtype_string.go b/logging/logspec/agreementtype_string.go index ee794f8b99..7f9cb45679 100644 --- a/logging/logspec/agreementtype_string.go +++ b/logging/logspec/agreementtype_string.go @@ -4,6 +4,39 @@ package logspec import "strconv" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[RoundConcluded-0] + _ = x[PeriodConcluded-1] + _ = x[StepTimeout-2] + _ = x[RoundStart-3] + _ = x[RoundInterrupted-4] + _ = x[RoundWaiting-5] + _ = x[ThresholdReached-6] + _ = x[BlockAssembled-7] + _ = x[BlockCommittable-8] + _ = x[ProposalAssembled-9] + _ = x[ProposalBroadcast-10] + _ = x[ProposalFrozen-11] + _ = x[ProposalAccepted-12] + _ = x[ProposalRejected-13] + _ = x[BlockRejected-14] + _ = x[BlockResent-15] + _ = x[BlockPipelined-16] + _ = x[VoteAttest-17] + _ = x[VoteBroadcast-18] + _ = x[VoteAccepted-19] + _ = x[VoteRejected-20] + _ = x[BundleBroadcast-21] + _ = x[BundleAccepted-22] + _ = x[BundleRejected-23] + _ = x[Restored-24] + _ = x[Persisted-25] + _ = x[numAgreementTypes-26] +} + const _AgreementType_name = "RoundConcludedPeriodConcludedStepTimeoutRoundStartRoundInterruptedRoundWaitingThresholdReachedBlockAssembledBlockCommittableProposalAssembledProposalBroadcastProposalFrozenProposalAcceptedProposalRejectedBlockRejectedBlockResentBlockPipelinedVoteAttestVoteBroadcastVoteAcceptedVoteRejectedBundleBroadcastBundleAcceptedBundleRejectedRestoredPersistednumAgreementTypes" var _AgreementType_index = [...]uint16{0, 14, 29, 40, 50, 66, 78, 94, 108, 124, 141, 158, 172, 188, 204, 217, 228, 242, 252, 265, 277, 289, 304, 318, 332, 340, 349, 366} diff --git a/logging/logspec/component_string.go b/logging/logspec/component_string.go index 91b1e4c03e..91490b95d9 100644 --- a/logging/logspec/component_string.go +++ b/logging/logspec/component_string.go @@ -4,6 +4,18 @@ package logspec import "strconv" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Agreement-0] + _ = x[Catchup-1] + _ = x[Network-2] + _ = x[Ledger-3] + _ = x[Frontend-4] + _ = x[numComponents-5] +} + const _Component_name = "AgreementCatchupNetworkLedgerFrontendnumComponents" var _Component_index = [...]uint8{0, 9, 16, 23, 29, 37, 50} diff --git a/logging/logspec/ledgertype_string.go b/logging/logspec/ledgertype_string.go index 9b1230b88a..81e62e2d8b 100644 --- a/logging/logspec/ledgertype_string.go +++ b/logging/logspec/ledgertype_string.go @@ -4,6 +4,14 @@ package logspec import "strconv" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[WroteBlock-0] + _ = x[numLedgerTypes-1] +} + const _LedgerType_name = "WroteBlocknumLedgerTypes" var _LedgerType_index = [...]uint8{0, 10, 24} diff --git a/protocol/codec.go b/protocol/codec.go index a65c76b8ac..2c850a5e7f 100644 --- a/protocol/codec.go +++ b/protocol/codec.go @@ -227,6 +227,11 @@ func NewEncoder(w io.Writer) *codec.Encoder { return codec.NewEncoder(w, CodecHandle) } +// NewJSONEncoder returns an encoder object writing bytes into [w]. +func NewJSONEncoder(w io.Writer) *codec.Encoder { + return codec.NewEncoder(w, JSONHandle) +} + // NewDecoder returns a decoder object reading bytes from [r]. func NewDecoder(r io.Reader) Decoder { return codec.NewDecoder(r, CodecHandle) diff --git a/protocol/txntype.go b/protocol/txntype.go index b1c7047be3..d8e50ecc00 100644 --- a/protocol/txntype.go +++ b/protocol/txntype.go @@ -38,6 +38,9 @@ const ( // AssetFreezeTx changes the freeze status of an asset AssetFreezeTx TxType = "afrz" + // ApplicationCallTx allows creating, deleting, and interacting with an application + ApplicationCallTx TxType = "appl" + // UnknownTx signals an error UnknownTx TxType = "unknown" ) From 4b257822f2bd5f4a561c7f6b62aea63e4d38c888 Mon Sep 17 00:00:00 2001 From: egieseke Date: Thu, 11 Jun 2020 15:57:05 -0400 Subject: [PATCH 047/267] Fixed a problem verifying recovered wallet in create wallet expect test. --- test/e2e-go/cli/goal/expect/createWalletTest.exp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/e2e-go/cli/goal/expect/createWalletTest.exp b/test/e2e-go/cli/goal/expect/createWalletTest.exp index 740af87a8b..117da37cda 100755 --- a/test/e2e-go/cli/goal/expect/createWalletTest.exp +++ b/test/e2e-go/cli/goal/expect/createWalletTest.exp @@ -136,7 +136,8 @@ if { [catch { # Create a new account under the recovered wallet to retrieve preexisting account set RECOVERED_ACCOUNT_ADDRESS [::AlgorandGoal::CreateAccountForWallet $RECOVERED_WALLET_NAME $NEW_WALLET_PASSWORD $TEST_PRIMARY_NODE_DIR] - ::AlgorandGoal::VerifyAccount $WALLET_NAME $WALLET_PASSWORD $RECOVERED_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR + puts "Recovered Account Address: $RECOVERED_ACCOUNT_ADDRESS" + ::AlgorandGoal::VerifyAccount $RECOVERED_WALLET_NAME $NEW_WALLET_PASSWORD $RECOVERED_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR if { $RECOVERED_ACCOUNT_ADDRESS != $ACCOUNT_ADDRESS } { ::AlgorandGoal::Abort "Expected recovered account address $ACCOUNT_ADDRESS, but found $RECOVERED_ACCOUNT_ADDRESS" @@ -145,7 +146,8 @@ if { [catch { # Create a new account under the recovered wallet, this should return a new account with zero balance since # only one account was created under the original wallet name. set RECOVERED_ACCOUNT_ADDRESS_2 [::AlgorandGoal::CreateAccountForWallet $RECOVERED_WALLET_NAME $NEW_WALLET_PASSWORD $TEST_PRIMARY_NODE_DIR] - ::AlgorandGoal::VerifyAccount $RECOVERED_WALLET_NAME $NEW_WALLET_PASSWORD #RECOVERED_ACCOUNT_ADDRESS_2 $TEST_PRIMARY_NODE_DIR + puts "Recovered Account Address 2: RECOVERED_ACCOUNT_ADDRESS_2" + ::AlgorandGoal::VerifyAccount $RECOVERED_WALLET_NAME $NEW_WALLET_PASSWORD $RECOVERED_ACCOUNT_ADDRESS_2 $TEST_PRIMARY_NODE_DIR set RECOVERED_BALANCE_2 [::AlgorandGoal::GetAccountBalance $RECOVERED_WALLET_NAME $RECOVERED_ACCOUNT_ADDRESS_2 $TEST_PRIMARY_NODE_DIR] if { $RECOVERED_BALANCE_2 != 0 } { ::AlgorandGoal::Abort "Expected recovered account address $RECOVERED_ACCOUNT_ADDRESS_2 with balance 0, but found $RECOVERED_BALANCE_2" From 8f09085f6ffb75818a8338c22626cbfcfad62433 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 11 Jun 2020 19:11:47 -0400 Subject: [PATCH 048/267] Add byteslice checking to the msgp decoder (#1151) Currently, we don't have a bounds on a byte slices in the msgp decoder. This change allows us to limit the size of a byte slice using the allocbound mechanism used for other slice/map types. Specifically, this change add the allocbound checking to the LogicSig and Notes field. --- config/consensus.go | 10 ++ data/transactions/logicsig.go | 4 +- data/transactions/msgp_gen.go | 237 +++++++++++++++++++++---------- data/transactions/transaction.go | 2 +- go.mod | 2 +- go.sum | 4 +- 6 files changed, 175 insertions(+), 84 deletions(-) diff --git a/config/consensus.go b/config/consensus.go index 6942bc208c..589adcdb2d 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -313,6 +313,14 @@ var MaxEvalDeltaAccounts int // in a StateDelta, used for decoding purposes. var MaxStateDeltaKeys int +// MaxLogicSigMaxSize is the largest logical signature appear in any of the supported +// protocols, used for decoding purposes. +var MaxLogicSigMaxSize int + +// MaxTxnNoteBytes is the largest supported nodes field array size supported by any +// of the consensus protocols. used for decoding purposes. +var MaxTxnNoteBytes int + func checkSetMax(value int, curMax *int) { if value > *curMax { *curMax = value @@ -335,6 +343,8 @@ func checkSetAllocBounds(p ConsensusParams) { // executed TEAL instructions should be fine (order of ~1000) checkSetMax(p.MaxAppProgramLen, &MaxStateDeltaKeys) checkSetMax(p.MaxAppProgramLen, &MaxEvalDeltaAccounts) + checkSetMax(int(p.LogicSigMaxSize), &MaxLogicSigMaxSize) + checkSetMax(p.MaxTxnNoteBytes, &MaxTxnNoteBytes) } // SaveConfigurableConsensus saves the configurable protocols file to the provided data directory. diff --git a/data/transactions/logicsig.go b/data/transactions/logicsig.go index 675847131e..9efdb844e4 100644 --- a/data/transactions/logicsig.go +++ b/data/transactions/logicsig.go @@ -33,13 +33,13 @@ type LogicSig struct { _struct struct{} `codec:",omitempty,omitemptyarray"` // Logic signed by Sig or Msig, OR hashed to be the Address of an account. - Logic []byte `codec:"l"` + Logic []byte `codec:"l,allocbound=config.MaxLogicSigMaxSize"` Sig crypto.Signature `codec:"sig"` Msig crypto.MultisigSig `codec:"msig"` // Args are not signed, but checked by Logic - Args [][]byte `codec:"arg,allocbound=EvalMaxArgs"` + Args [][]byte `codec:"arg,allocbound=EvalMaxArgs,allocbound=config.MaxLogicSigMaxSize"` } // Blank returns true if there is no content in this LogicSig diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index 623ebc4036..904a74aa9a 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -3,6 +3,7 @@ package transactions // Code generated by github.com/algorand/msgp DO NOT EDIT. import ( + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/msgp/msgp" @@ -1543,6 +1544,16 @@ func (z *Header) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0002 > 0 { zb0002-- + var zb0004 int + zb0004, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Note") + return + } + if zb0004 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0004), uint64(config.MaxTxnNoteBytes)) + return + } (*z).Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Note) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Note") @@ -1637,6 +1648,16 @@ func (z *Header) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "note": + var zb0005 int + zb0005, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "Note") + return + } + if zb0005 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0005), uint64(config.MaxTxnNoteBytes)) + return + } (*z).Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Note) if err != nil { err = msgp.WrapError(err, "Note") @@ -2022,6 +2043,16 @@ func (z *LogicSig) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0002 > 0 { zb0002-- + var zb0004 int + zb0004, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Logic") + return + } + if zb0004 > config.MaxLogicSigMaxSize { + err = msgp.ErrOverflow(uint64(zb0004), uint64(config.MaxLogicSigMaxSize)) + return + } (*z).Logic, bts, err = msgp.ReadBytesBytes(bts, (*z).Logic) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Logic") @@ -2046,26 +2077,36 @@ func (z *LogicSig) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0002 > 0 { zb0002-- - var zb0004 int - var zb0005 bool - zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Args") return } - if zb0004 > EvalMaxArgs { - err = msgp.ErrOverflow(uint64(zb0004), uint64(EvalMaxArgs)) + if zb0005 > EvalMaxArgs { + err = msgp.ErrOverflow(uint64(zb0005), uint64(EvalMaxArgs)) err = msgp.WrapError(err, "struct-from-array", "Args") return } - if zb0005 { + if zb0006 { (*z).Args = nil - } else if (*z).Args != nil && cap((*z).Args) >= zb0004 { - (*z).Args = ((*z).Args)[:zb0004] + } else if (*z).Args != nil && cap((*z).Args) >= zb0005 { + (*z).Args = ((*z).Args)[:zb0005] } else { - (*z).Args = make([][]byte, zb0004) + (*z).Args = make([][]byte, zb0005) } for zb0001 := range (*z).Args { + var zb0007 int + zb0007, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Args", zb0001) + return + } + if zb0007 > config.MaxLogicSigMaxSize { + err = msgp.ErrOverflow(uint64(zb0007), uint64(config.MaxLogicSigMaxSize)) + return + } (*z).Args[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).Args[zb0001]) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Args", zb0001) @@ -2097,6 +2138,16 @@ func (z *LogicSig) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "l": + var zb0008 int + zb0008, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "Logic") + return + } + if zb0008 > config.MaxLogicSigMaxSize { + err = msgp.ErrOverflow(uint64(zb0008), uint64(config.MaxLogicSigMaxSize)) + return + } (*z).Logic, bts, err = msgp.ReadBytesBytes(bts, (*z).Logic) if err != nil { err = msgp.WrapError(err, "Logic") @@ -2115,26 +2166,36 @@ func (z *LogicSig) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "arg": - var zb0006 int - var zb0007 bool - zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Args") return } - if zb0006 > EvalMaxArgs { - err = msgp.ErrOverflow(uint64(zb0006), uint64(EvalMaxArgs)) + if zb0009 > EvalMaxArgs { + err = msgp.ErrOverflow(uint64(zb0009), uint64(EvalMaxArgs)) err = msgp.WrapError(err, "Args") return } - if zb0007 { + if zb0010 { (*z).Args = nil - } else if (*z).Args != nil && cap((*z).Args) >= zb0006 { - (*z).Args = ((*z).Args)[:zb0006] + } else if (*z).Args != nil && cap((*z).Args) >= zb0009 { + (*z).Args = ((*z).Args)[:zb0009] } else { - (*z).Args = make([][]byte, zb0006) + (*z).Args = make([][]byte, zb0009) } for zb0001 := range (*z).Args { + var zb0011 int + zb0011, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "Args", zb0001) + return + } + if zb0011 > config.MaxLogicSigMaxSize { + err = msgp.ErrOverflow(uint64(zb0011), uint64(config.MaxLogicSigMaxSize)) + return + } (*z).Args[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).Args[zb0001]) if err != nil { err = msgp.WrapError(err, "Args", zb0001) @@ -4033,6 +4094,16 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0005 > 0 { zb0005-- + var zb0007 int + zb0007, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Note") + return + } + if zb0007 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0007), uint64(config.MaxTxnNoteBytes)) + return + } (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Note") @@ -4242,35 +4313,35 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { if zb0005 > 0 { zb0005-- { - var zb0007 uint64 - zb0007, bts, err = msgp.ReadUint64Bytes(bts) + var zb0008 uint64 + zb0008, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "OnCompletion") return } - (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0007) + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0008) } } if zb0005 > 0 { zb0005-- - var zb0008 int - var zb0009 bool - zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0008 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0008), uint64(encodedMaxApplicationArgs)) + if zb0009 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0009 { + if zb0010 { (*z).ApplicationCallTxnFields.ApplicationArgs = nil - } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0008 { - (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0008] + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0009 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0009] } else { - (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0008) + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0009) } for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) @@ -4282,24 +4353,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0005 > 0 { zb0005-- - var zb0010 int - var zb0011 bool - zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0010 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxAccounts)) + if zb0011 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0011 { + if zb0012 { (*z).ApplicationCallTxnFields.Accounts = nil - } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0010 { - (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0010] + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0011 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0011] } else { - (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0010) + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0011) } for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) @@ -4311,24 +4382,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0005 > 0 { zb0005-- - var zb0012 int - var zb0013 bool - zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0013 int + var zb0014 bool + zb0013, zb0014, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0012 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxForeignApps)) + if zb0013 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0013), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0013 { + if zb0014 { (*z).ApplicationCallTxnFields.ForeignApps = nil - } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0012 { - (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0012] + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0013 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0013] } else { - (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0012) + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0013) } for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) @@ -4424,6 +4495,16 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "note": + var zb0015 int + zb0015, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "Note") + return + } + if zb0015 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxTxnNoteBytes)) + return + } (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) if err != nil { err = msgp.WrapError(err, "Note") @@ -4581,33 +4662,33 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } case "apan": { - var zb0014 uint64 - zb0014, bts, err = msgp.ReadUint64Bytes(bts) + var zb0016 uint64 + zb0016, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "OnCompletion") return } - (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0014) + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0016) } case "apaa": - var zb0015 int - var zb0016 bool - zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0015 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0015), uint64(encodedMaxApplicationArgs)) + if zb0017 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0017), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0016 { + if zb0018 { (*z).ApplicationCallTxnFields.ApplicationArgs = nil - } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0015 { - (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0015] + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0017 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0017] } else { - (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0015) + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0017) } for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) @@ -4617,24 +4698,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apat": - var zb0017 int - var zb0018 bool - zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Accounts") return } - if zb0017 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0017), uint64(encodedMaxAccounts)) + if zb0019 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "Accounts") return } - if zb0018 { + if zb0020 { (*z).ApplicationCallTxnFields.Accounts = nil - } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0017 { - (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0017] + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0019 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0019] } else { - (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0017) + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0019) } for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) @@ -4644,24 +4725,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apfa": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0021 int + var zb0022 bool + zb0021, zb0022, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ForeignApps") return } - if zb0019 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxForeignApps)) + if zb0021 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "ForeignApps") return } - if zb0020 { + if zb0022 { (*z).ApplicationCallTxnFields.ForeignApps = nil - } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0019 { - (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0019] + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0021 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0021] } else { - (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0019) + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0021) } for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 4300a46956..3f1eabffa3 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -94,7 +94,7 @@ type Header struct { Fee basics.MicroAlgos `codec:"fee"` FirstValid basics.Round `codec:"fv"` LastValid basics.Round `codec:"lv"` - Note []byte `codec:"note"` // Uniqueness or app-level data about txn + Note []byte `codec:"note,allocbound=config.MaxTxnNoteBytes"` // Uniqueness or app-level data about txn GenesisID string `codec:"gen"` GenesisHash crypto.Digest `codec:"gh"` diff --git a/go.mod b/go.mod index ae701c45fc..b76365f20a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 - github.com/algorand/msgp v1.1.43 + github.com/algorand/msgp v1.1.45 github.com/algorand/oapi-codegen v1.3.5-algorand4 github.com/algorand/websocket v1.4.1 github.com/aws/aws-sdk-go v1.16.5 diff --git a/go.sum b/go.sum index a3b151b2fe..e7c015b44c 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d h1:W9MgGUo github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d/go.mod h1:qm6LyXvDa1+uZJxaVg8X+OEjBqt/zDinDa2EohtTDxU= github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 h1:IiCuOE1YCReVyEr1IQHKTBTvFLKdeBCfQuxrqhniq+I= github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759/go.mod h1:Kve3O9VpxZIHsPzpfxNdyFltFU9jBTeVYMYxSC99tdg= -github.com/algorand/msgp v1.1.43 h1:VPGGwJ5zIg9lRqgL23irmfcxgvgho1dpx7Lq6X2sn8g= -github.com/algorand/msgp v1.1.43/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= +github.com/algorand/msgp v1.1.45 h1:uLEPvg0BfTrb2JjBcXexON8il4vv0EsD7HYFaA7e5jg= +github.com/algorand/msgp v1.1.45/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= github.com/algorand/oapi-codegen v1.3.5-algorand4 h1:skjv/lhlWAGnQPHQ9qdzTE5Bk9W555EKrh1bSul0JJs= github.com/algorand/oapi-codegen v1.3.5-algorand4/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= github.com/algorand/websocket v1.4.1 h1:FPoNHI8i2VZWZzhCscY8JTzsAE7Vv73753cMbzb3udk= From 1b8ebd0e4c195264933dc92b2831f8cb9e700a21 Mon Sep 17 00:00:00 2001 From: egieseke Date: Thu, 11 Jun 2020 19:53:35 -0400 Subject: [PATCH 049/267] Eric/fix keyreg teal test (#1165) Fixed bug in key registration teal test --- test/scripts/e2e_subs/e2e_teal.sh | 2 +- test/scripts/e2e_subs/keyreg-teal-test.sh | 6 +++--- test/scripts/e2e_subs/limit-swap-test.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/scripts/e2e_subs/e2e_teal.sh b/test/scripts/e2e_subs/e2e_teal.sh index ebde271fac..534f3b91e8 100755 --- a/test/scripts/e2e_subs/e2e_teal.sh +++ b/test/scripts/e2e_subs/e2e_teal.sh @@ -65,7 +65,7 @@ ${gcmd} clerk send --amount 1000000 --from ${ACCOUNT} --to ${ACCOUNT_TLHC} # timeout round should pass. some of the 35 seconds was eaten by prior ops. CROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') while [ $CROUND -lt $TIMEOUT_ROUND ]; do - goal node wait + goal node wait --waittime 30 CROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') done diff --git a/test/scripts/e2e_subs/keyreg-teal-test.sh b/test/scripts/e2e_subs/keyreg-teal-test.sh index b807fe4841..0dde49eaab 100755 --- a/test/scripts/e2e_subs/keyreg-teal-test.sh +++ b/test/scripts/e2e_subs/keyreg-teal-test.sh @@ -39,8 +39,8 @@ cat< ${TEMPDIR}/pbound print(((${ROUND} // ${PERIOD}) * ${PERIOD}) + ${PERIOD}) EOF PBOUND=$(cat ${TEMPDIR}/pbound) -while [ ${ROUND} != ${PBOUND} ]; do - goal node wait +while [ ${ROUND} -lt ${PBOUND} ]; do + goal node wait --waittime 30 ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') done @@ -127,7 +127,7 @@ fi echo "wait for valid duration to pass" ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') while [ ${ROUND} -lt `expr ${EXPIRE} + 1` ]; do - goal node wait + goal node wait --waittime 30 ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') done diff --git a/test/scripts/e2e_subs/limit-swap-test.sh b/test/scripts/e2e_subs/limit-swap-test.sh index 9a7c2acb65..72084b1eeb 100755 --- a/test/scripts/e2e_subs/limit-swap-test.sh +++ b/test/scripts/e2e_subs/limit-swap-test.sh @@ -68,7 +68,7 @@ ${gcmd} asset send --assetid ${ASSET_ID} -f ${ACCOUNT} -t ${ACCOUNT_ASSET_TRADER ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') while [ $ROUND -lt $TIMEOUT_ROUND ]; do - goal node wait + goal node wait --waittime 30 ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') done From 84de48304f91f754f363d683e1e32590475918e0 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 11 Jun 2020 21:09:00 -0400 Subject: [PATCH 050/267] Bugfix: Better algoh parameter tunneling (#1166) Algoh is a host process, intended to monitor the child algod process. On algoh invocation, it starts the algod with the same input argument it was called upon. Prior to this change, the following command line options, which were supported by algod would have been blocked by algoh: -p -l -seed -g --- cmd/algoh/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/algoh/main.go b/cmd/algoh/main.go index bc533cc970..22cdf3189f 100644 --- a/cmd/algoh/main.go +++ b/cmd/algoh/main.go @@ -44,6 +44,14 @@ var dataDirectory = flag.String("d", "", "Root Algorand daemon data path") var versionCheck = flag.Bool("v", false, "Display and write current build version and exit") var telemetryOverride = flag.String("t", "", `Override telemetry setting if supported (Use "true", "false", "0" or "1")`) +// the following flags aren't being used by the algoh, but are needed so that the flag package won't complain that +// these flags were provided but were not defined. We grab all the input flags and pass these downstream to the algod executable +// as an input arguments. +var peerOverride = flag.String("p", "", "Override phonebook with peer ip:port (or semicolon separated list: ip:port;ip:port;ip:port...)") +var listenIP = flag.String("l", "", "Override config.EndpointAddress (REST listening address) with ip:port") +var seed = flag.String("seed", "", "input to math/rand.Seed()") +var genesisFile = flag.String("g", "", "Genesis configuration file") + const algodFileName = "algod" const goalFileName = "goal" From f4681a404aa3f1082b064f44cab0b88ea8a34572 Mon Sep 17 00:00:00 2001 From: John Lee Date: Fri, 12 Jun 2020 14:37:20 -0400 Subject: [PATCH 051/267] Fix path to gpg-fake.sh script --- scripts/release/test/stage/test/task.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/test/stage/test/task.sh b/scripts/release/test/stage/test/task.sh index 77805bb04c..29113e231d 100755 --- a/scripts/release/test/stage/test/task.sh +++ b/scripts/release/test/stage/test/task.sh @@ -9,7 +9,7 @@ date "+build_release begin TEST stage %Y%m%d_%H%M%S" echo echo Setup GPG -"${HOME}"/go/src/github.com/algorand/go-algorand/scripts/release/test/gpg-fake.sh +"${HOME}"/go/src/github.com/algorand/go-algorand/scripts/release/test/util/gpg-fake.sh "${HOME}"/go/src/github.com/algorand/go-algorand/scripts/release/test/deb/run_ubuntu.sh date "+build_release done testing ubuntu %Y%m%d_%H%M%S" From b81994e0f2a0b4372e9c22673f6458c685e2f593 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 12 Jun 2020 15:58:41 -0400 Subject: [PATCH 052/267] Update code to take advantage of byteslice allocbounding (#1167) Add a allocbound for the byte slices definition in our code that gets message-packed. The unit testing is verifying that we didn't miss any additional instance. --- config/consensus.go | 15 +++ data/basics/msgp_gen.go | 180 +++++++++++++++---------- data/basics/userBalance.go | 4 +- data/bookkeeping/genesis.go | 10 +- data/bookkeeping/msgp_gen.go | 10 ++ data/transactions/application.go | 4 +- data/transactions/msgp_gen.go | 218 ++++++++++++++++++++++--------- data/transactions/transaction.go | 2 +- protocol/codec_tester.go | 77 ++++++++++- 9 files changed, 373 insertions(+), 147 deletions(-) diff --git a/config/consensus.go b/config/consensus.go index 589adcdb2d..dcc675bf73 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -321,6 +321,14 @@ var MaxLogicSigMaxSize int // of the consensus protocols. used for decoding purposes. var MaxTxnNoteBytes int +// MaxTxGroupSize is the largest supported number of transactions per transaction group supported by any +// of the consensus protocols. used for decoding purposes. +var MaxTxGroupSize int + +// MaxAppProgramLen is the largest supported app program size supported by any +// of the consensus protocols. used for decoding purposes. +var MaxAppProgramLen int + func checkSetMax(value int, curMax *int) { if value > *curMax { *curMax = value @@ -343,8 +351,11 @@ func checkSetAllocBounds(p ConsensusParams) { // executed TEAL instructions should be fine (order of ~1000) checkSetMax(p.MaxAppProgramLen, &MaxStateDeltaKeys) checkSetMax(p.MaxAppProgramLen, &MaxEvalDeltaAccounts) + + checkSetMax(p.MaxAppProgramLen, &MaxAppProgramLen) checkSetMax(int(p.LogicSigMaxSize), &MaxLogicSigMaxSize) checkSetMax(p.MaxTxnNoteBytes, &MaxTxnNoteBytes) + checkSetMax(p.MaxTxGroupSize, &MaxTxGroupSize) } // SaveConfigurableConsensus saves the configurable protocols file to the provided data directory. @@ -409,6 +420,10 @@ func LoadConfigurableConsensusProtocols(dataDirectory string) error { } if newConsensus != nil { Consensus = newConsensus + // Set allocation limits + for _, p := range Consensus { + checkSetAllocBounds(p) + } } return nil } diff --git a/data/basics/msgp_gen.go b/data/basics/msgp_gen.go index a088426460..fce4eb73d3 100644 --- a/data/basics/msgp_gen.go +++ b/data/basics/msgp_gen.go @@ -1882,6 +1882,16 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- + var zb0005 int + zb0005, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + if zb0005 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0005), uint64(config.MaxAppProgramLen)) + return + } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") @@ -1890,6 +1900,16 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- + var zb0006 int + zb0006, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + if zb0006 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0006), uint64(config.MaxAppProgramLen)) + return + } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") @@ -1898,33 +1918,33 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } - if zb0005 > 0 { - zb0005-- + if zb0007 > 0 { + zb0007-- (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumUint") return } } - if zb0005 > 0 { - zb0005-- + if zb0007 > 0 { + zb0007-- (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0005 > 0 { - err = msgp.ErrTooManyArrayFields(zb0005) + if zb0007 > 0 { + err = msgp.ErrTooManyArrayFields(zb0007) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array") return @@ -1935,11 +1955,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } - if zb0006 { + if zb0008 { (*z).LocalStateSchema = StateSchema{} } - for zb0005 > 0 { - zb0005-- + for zb0007 > 0 { + zb0007-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") @@ -1970,33 +1990,33 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- - var zb0007 int - var zb0008 bool - zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } - if zb0007 > 0 { - zb0007-- + if zb0009 > 0 { + zb0009-- (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumUint") return } } - if zb0007 > 0 { - zb0007-- + if zb0009 > 0 { + zb0009-- (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0007 > 0 { - err = msgp.ErrTooManyArrayFields(zb0007) + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array") return @@ -2007,11 +2027,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } - if zb0008 { + if zb0010 { (*z).GlobalStateSchema = StateSchema{} } - for zb0007 > 0 { - zb0007-- + for zb0009 > 0 { + zb0009-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") @@ -2042,27 +2062,27 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- - var zb0009 int - var zb0010 bool - zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalState") return } - if zb0009 > encodedMaxKeyValueEntries { - err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxKeyValueEntries)) + if zb0011 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxKeyValueEntries)) err = msgp.WrapError(err, "struct-from-array", "GlobalState") return } - if zb0010 { + if zb0012 { (*z).GlobalState = nil } else if (*z).GlobalState == nil { - (*z).GlobalState = make(TealKeyValue, zb0009) + (*z).GlobalState = make(TealKeyValue, zb0011) } - for zb0009 > 0 { + for zb0011 > 0 { var zb0001 string var zb0002 TealValue - zb0009-- + zb0011-- zb0001, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalState") @@ -2100,45 +2120,65 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "approv": + var zb0013 int + zb0013, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + if zb0013 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0013), uint64(config.MaxAppProgramLen)) + return + } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { err = msgp.WrapError(err, "ApprovalProgram") return } case "clearp": + var zb0014 int + zb0014, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + if zb0014 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0014), uint64(config.MaxAppProgramLen)) + return + } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { err = msgp.WrapError(err, "ClearStateProgram") return } case "lsch": - var zb0011 int - var zb0012 bool - zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0015 int + var zb0016 bool + zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema") return } - if zb0011 > 0 { - zb0011-- + if zb0015 > 0 { + zb0015-- (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumUint") return } } - if zb0011 > 0 { - zb0011-- + if zb0015 > 0 { + zb0015-- (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0011 > 0 { - err = msgp.ErrTooManyArrayFields(zb0011) + if zb0015 > 0 { + err = msgp.ErrTooManyArrayFields(zb0015) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array") return @@ -2149,11 +2189,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "LocalStateSchema") return } - if zb0012 { + if zb0016 { (*z).LocalStateSchema = StateSchema{} } - for zb0011 > 0 { - zb0011-- + for zb0015 > 0 { + zb0015-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema") @@ -2182,33 +2222,33 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "gsch": - var zb0013 int - var zb0014 bool - zb0013, zb0014, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0013, zb0014, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema") return } - if zb0013 > 0 { - zb0013-- + if zb0017 > 0 { + zb0017-- (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumUint") return } } - if zb0013 > 0 { - zb0013-- + if zb0017 > 0 { + zb0017-- (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0013 > 0 { - err = msgp.ErrTooManyArrayFields(zb0013) + if zb0017 > 0 { + err = msgp.ErrTooManyArrayFields(zb0017) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array") return @@ -2219,11 +2259,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "GlobalStateSchema") return } - if zb0014 { + if zb0018 { (*z).GlobalStateSchema = StateSchema{} } - for zb0013 > 0 { - zb0013-- + for zb0017 > 0 { + zb0017-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema") @@ -2252,27 +2292,27 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "gs": - var zb0015 int - var zb0016 bool - zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalState") return } - if zb0015 > encodedMaxKeyValueEntries { - err = msgp.ErrOverflow(uint64(zb0015), uint64(encodedMaxKeyValueEntries)) + if zb0019 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxKeyValueEntries)) err = msgp.WrapError(err, "GlobalState") return } - if zb0016 { + if zb0020 { (*z).GlobalState = nil } else if (*z).GlobalState == nil { - (*z).GlobalState = make(TealKeyValue, zb0015) + (*z).GlobalState = make(TealKeyValue, zb0019) } - for zb0015 > 0 { + for zb0019 > 0 { var zb0001 string var zb0002 TealValue - zb0015-- + zb0019-- zb0001, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalState") diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index 1c93c0946a..b14fdcafb5 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -200,8 +200,8 @@ type AppLocalState struct { type AppParams struct { _struct struct{} `codec:",omitempty,omitemptyarray"` - ApprovalProgram []byte `codec:"approv"` - ClearStateProgram []byte `codec:"clearp"` + ApprovalProgram []byte `codec:"approv,allocbound=config.MaxAppProgramLen"` + ClearStateProgram []byte `codec:"clearp,allocbound=config.MaxAppProgramLen"` LocalStateSchema StateSchema `codec:"lsch"` GlobalStateSchema StateSchema `codec:"gsch"` diff --git a/data/bookkeeping/genesis.go b/data/bookkeeping/genesis.go index af7c2f6b72..a28b83f7b6 100644 --- a/data/bookkeeping/genesis.go +++ b/data/bookkeeping/genesis.go @@ -23,6 +23,14 @@ import ( "github.com/algorand/go-algorand/protocol" ) +const ( + // MaxInitialGenesisAllocationSize is the maximum number of accounts that are supported when + // bootstrapping a new network. The number of account *can* grow further after the bootstrapping. + // This value is used exclusively for the messagepack decoder, and has no affect on the network + // capabilities/capacity in any way. + MaxInitialGenesisAllocationSize = 100000000 +) + // A Genesis object defines an Algorand "universe" -- a set of nodes that can // talk to each other, agree on the ledger contents, etc. This is defined // by the initial account states (GenesisAllocation), the initial @@ -47,7 +55,7 @@ type Genesis struct { Proto protocol.ConsensusVersion `codec:"proto"` // Allocation determines the initial accounts and their state. - Allocation []GenesisAllocation `codec:"alloc,allocbound=-"` + Allocation []GenesisAllocation `codec:"alloc,allocbound=MaxInitialGenesisAllocationSize"` // RewardsPool is the address of the rewards pool. RewardsPool string `codec:"rwd"` diff --git a/data/bookkeeping/msgp_gen.go b/data/bookkeeping/msgp_gen.go index 06d61d8ca1..a93dfe395e 100644 --- a/data/bookkeeping/msgp_gen.go +++ b/data/bookkeeping/msgp_gen.go @@ -1565,6 +1565,11 @@ func (z *Genesis) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "Allocation") return } + if zb0004 > MaxInitialGenesisAllocationSize { + err = msgp.ErrOverflow(uint64(zb0004), uint64(MaxInitialGenesisAllocationSize)) + err = msgp.WrapError(err, "struct-from-array", "Allocation") + return + } if zb0005 { (*z).Allocation = nil } else if (*z).Allocation != nil && cap((*z).Allocation) >= zb0004 { @@ -1661,6 +1666,11 @@ func (z *Genesis) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "Allocation") return } + if zb0006 > MaxInitialGenesisAllocationSize { + err = msgp.ErrOverflow(uint64(zb0006), uint64(MaxInitialGenesisAllocationSize)) + err = msgp.WrapError(err, "Allocation") + return + } if zb0007 { (*z).Allocation = nil } else if (*z).Allocation != nil && cap((*z).Allocation) >= zb0006 { diff --git a/data/transactions/application.go b/data/transactions/application.go index c6852364ee..ee3c1aadae 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -88,8 +88,8 @@ type ApplicationCallTxnFields struct { LocalStateSchema basics.StateSchema `codec:"apls"` GlobalStateSchema basics.StateSchema `codec:"apgs"` - ApprovalProgram []byte `codec:"apap"` - ClearStateProgram []byte `codec:"apsu"` + ApprovalProgram []byte `codec:"apap,allocbound=config.MaxAppProgramLen"` + ClearStateProgram []byte `codec:"apsu,allocbound=config.MaxAppProgramLen"` // If you add any fields here, remember you MUST modify the Empty // method below! diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index 904a74aa9a..a046aaf7ae 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -433,6 +433,16 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } if zb0004 > 0 { zb0004-- + var zb0013 int + zb0013, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + if zb0013 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0013), uint64(config.MaxAppProgramLen)) + return + } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") @@ -441,6 +451,16 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } if zb0004 > 0 { zb0004-- + var zb0014 int + zb0014, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + if zb0014 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0014), uint64(config.MaxAppProgramLen)) + return + } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") @@ -478,33 +498,33 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } case "apan": { - var zb0013 uint64 - zb0013, bts, err = msgp.ReadUint64Bytes(bts) + var zb0015 uint64 + zb0015, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "OnCompletion") return } - (*z).OnCompletion = OnCompletion(zb0013) + (*z).OnCompletion = OnCompletion(zb0015) } case "apaa": - var zb0014 int - var zb0015 bool - zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0014 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxApplicationArgs)) + if zb0016 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0015 { + if zb0017 { (*z).ApplicationArgs = nil - } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0014 { - (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0014] + } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0016 { + (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0016] } else { - (*z).ApplicationArgs = make([][]byte, zb0014) + (*z).ApplicationArgs = make([][]byte, zb0016) } for zb0001 := range (*z).ApplicationArgs { (*z).ApplicationArgs[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationArgs[zb0001]) @@ -514,24 +534,24 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } case "apat": - var zb0016 int - var zb0017 bool - zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0018 int + var zb0019 bool + zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Accounts") return } - if zb0016 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxAccounts)) + if zb0018 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0018), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "Accounts") return } - if zb0017 { + if zb0019 { (*z).Accounts = nil - } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0016 { - (*z).Accounts = ((*z).Accounts)[:zb0016] + } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0018 { + (*z).Accounts = ((*z).Accounts)[:zb0018] } else { - (*z).Accounts = make([]basics.Address, zb0016) + (*z).Accounts = make([]basics.Address, zb0018) } for zb0002 := range (*z).Accounts { bts, err = (*z).Accounts[zb0002].UnmarshalMsg(bts) @@ -541,24 +561,24 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } case "apfa": - var zb0018 int - var zb0019 bool - zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0020 int + var zb0021 bool + zb0020, zb0021, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ForeignApps") return } - if zb0018 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0018), uint64(encodedMaxForeignApps)) + if zb0020 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0020), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "ForeignApps") return } - if zb0019 { + if zb0021 { (*z).ForeignApps = nil - } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0018 { - (*z).ForeignApps = ((*z).ForeignApps)[:zb0018] + } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0020 { + (*z).ForeignApps = ((*z).ForeignApps)[:zb0020] } else { - (*z).ForeignApps = make([]basics.AppIndex, zb0018) + (*z).ForeignApps = make([]basics.AppIndex, zb0020) } for zb0003 := range (*z).ForeignApps { bts, err = (*z).ForeignApps[zb0003].UnmarshalMsg(bts) @@ -580,12 +600,32 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } case "apap": + var zb0022 int + zb0022, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + if zb0022 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0022), uint64(config.MaxAppProgramLen)) + return + } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) if err != nil { err = msgp.WrapError(err, "ApprovalProgram") return } case "apsu": + var zb0023 int + zb0023, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + if zb0023 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0023), uint64(config.MaxAppProgramLen)) + return + } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) if err != nil { err = msgp.WrapError(err, "ClearStateProgram") @@ -4427,6 +4467,16 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0005 > 0 { zb0005-- + var zb0015 int + zb0015, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") + return + } + if zb0015 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxAppProgramLen)) + return + } (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") @@ -4435,6 +4485,16 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0005 > 0 { zb0005-- + var zb0016 int + zb0016, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") + return + } + if zb0016 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0016), uint64(config.MaxAppProgramLen)) + return + } (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") @@ -4495,14 +4555,14 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "note": - var zb0015 int - zb0015, err = msgp.ReadBytesBytesHeader(bts) + var zb0017 int + zb0017, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "Note") return } - if zb0015 > config.MaxTxnNoteBytes { - err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxTxnNoteBytes)) + if zb0017 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxTxnNoteBytes)) return } (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) @@ -4662,33 +4722,33 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } case "apan": { - var zb0016 uint64 - zb0016, bts, err = msgp.ReadUint64Bytes(bts) + var zb0018 uint64 + zb0018, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "OnCompletion") return } - (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0016) + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0018) } case "apaa": - var zb0017 int - var zb0018 bool - zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0017 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0017), uint64(encodedMaxApplicationArgs)) + if zb0019 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0018 { + if zb0020 { (*z).ApplicationCallTxnFields.ApplicationArgs = nil - } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0017 { - (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0017] + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0019 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0019] } else { - (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0017) + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0019) } for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) @@ -4698,24 +4758,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apat": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0021 int + var zb0022 bool + zb0021, zb0022, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Accounts") return } - if zb0019 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxAccounts)) + if zb0021 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "Accounts") return } - if zb0020 { + if zb0022 { (*z).ApplicationCallTxnFields.Accounts = nil - } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0019 { - (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0019] + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0021 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0021] } else { - (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0019) + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0021) } for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) @@ -4725,24 +4785,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apfa": - var zb0021 int - var zb0022 bool - zb0021, zb0022, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0023 int + var zb0024 bool + zb0023, zb0024, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ForeignApps") return } - if zb0021 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxForeignApps)) + if zb0023 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0023), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "ForeignApps") return } - if zb0022 { + if zb0024 { (*z).ApplicationCallTxnFields.ForeignApps = nil - } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0021 { - (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0021] + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0023 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0023] } else { - (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0021) + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0023) } for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) @@ -4764,12 +4824,32 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "apap": + var zb0025 int + zb0025, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ApprovalProgram") + return + } + if zb0025 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0025), uint64(config.MaxAppProgramLen)) + return + } (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) if err != nil { err = msgp.WrapError(err, "ApprovalProgram") return } case "apsu": + var zb0026 int + zb0026, err = msgp.ReadBytesBytesHeader(bts) + if err != nil { + err = msgp.WrapError(err, "ClearStateProgram") + return + } + if zb0026 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0026), uint64(config.MaxAppProgramLen)) + return + } (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) if err != nil { err = msgp.WrapError(err, "ClearStateProgram") @@ -4876,6 +4956,11 @@ func (z *TxGroup) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "TxGroupHashes") return } + if zb0004 > config.MaxTxGroupSize { + err = msgp.ErrOverflow(uint64(zb0004), uint64(config.MaxTxGroupSize)) + err = msgp.WrapError(err, "struct-from-array", "TxGroupHashes") + return + } if zb0005 { (*z).TxGroupHashes = nil } else if (*z).TxGroupHashes != nil && cap((*z).TxGroupHashes) >= zb0004 { @@ -4922,6 +5007,11 @@ func (z *TxGroup) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "TxGroupHashes") return } + if zb0006 > config.MaxTxGroupSize { + err = msgp.ErrOverflow(uint64(zb0006), uint64(config.MaxTxGroupSize)) + err = msgp.WrapError(err, "TxGroupHashes") + return + } if zb0007 { (*z).TxGroupHashes = nil } else if (*z).TxGroupHashes != nil && cap((*z).TxGroupHashes) >= zb0006 { diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 3f1eabffa3..1c15871fb8 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -180,7 +180,7 @@ type TxGroup struct { // together, sequentially, in a block in order for the group to be // valid. Each hash in the list is a hash of a transaction with // the `Group` field omitted. - TxGroupHashes []crypto.Digest `codec:"txlist,allocbound=-"` + TxGroupHashes []crypto.Digest `codec:"txlist,allocbound=config.MaxTxGroupSize"` } // ToBeHashed implements the crypto.Hashable interface. diff --git a/protocol/codec_tester.go b/protocol/codec_tester.go index 9c140615a8..4f017a29e4 100644 --- a/protocol/codec_tester.go +++ b/protocol/codec_tester.go @@ -20,6 +20,9 @@ import ( "fmt" "io/ioutil" "math/rand" + "os" + "path" + "path/filepath" "reflect" "strings" "testing" @@ -84,20 +87,74 @@ func printWarning(warnMsg string) { } } -func checkBoundsLimitingTag(objType string, datapath string, structTag string) { +var testedDatatypesForAllocBound = map[string]bool{} +var testedDatatypesForAllocBoundMu = deadlock.Mutex{} + +func checkBoundsLimitingTag(val reflect.Value, datapath string, structTag string) (hasAllocBound bool) { if structTag == "" { return } - tagsMap := parseStructTags(structTag) - if _, have := tagsMap["allocbound"]; !have { - printWarning(fmt.Sprintf("%s %s does not have an allocbound defined", objType, datapath)) - return + testedDatatypesForAllocBoundMu.Lock() + defer testedDatatypesForAllocBoundMu.Unlock() + // make sure we test each datatype only once. + if val.Type().Name() == "" { + if testedDatatypesForAllocBound[datapath] { + hasAllocBound = true + return + } + testedDatatypesForAllocBound[datapath] = true + } else { + if testedDatatypesForAllocBound[val.Type().Name()] { + hasAllocBound = true + return + } + testedDatatypesForAllocBound[val.Type().Name()] = true + } + + var objType string + if val.Kind() == reflect.Slice { + objType = "slice" + } else if val.Kind() == reflect.Map { + objType = "map" } + + tagsMap := parseStructTags(structTag) + if tagsMap["allocbound"] == "-" { printWarning(fmt.Sprintf("%s %s have an unbounded allocbound defined", objType, datapath)) return } + if _, have := tagsMap["allocbound"]; have { + hasAllocBound = true + return + } + + if val.Type().Name() != "" { + // does any of the go files in the package directroy has the msgp:allocbound defined for that datatype ? + gopath := os.Getenv("GOPATH") + packageFilesPath := path.Join(gopath, "src", val.Type().PkgPath()) + packageFiles := []string{} + filepath.Walk(packageFilesPath, func(path string, info os.FileInfo, err error) error { + if filepath.Ext(path) == ".go" { + packageFiles = append(packageFiles, path) + } + return nil + }) + for _, packageFile := range packageFiles { + fileBytes, err := ioutil.ReadFile(packageFile) + if err != nil { + continue + } + if strings.Index(string(fileBytes), fmt.Sprintf("msgp:allocbound %s", val.Type().Name())) != -1 { + // message pack alloc bound definition was found. + hasAllocBound = true + return + } + } + } + printWarning(fmt.Sprintf("%s %s does not have an allocbound defined - %s", objType, datapath, val.Type().PkgPath())) + return } func randomizeValue(v reflect.Value, datapath string, tag string) error { @@ -144,8 +201,11 @@ func randomizeValue(v reflect.Value, datapath string, tag string) error { } } case reflect.Slice: - checkBoundsLimitingTag("slice", datapath, tag) + hasAllocBound := checkBoundsLimitingTag(v, datapath, tag) l := rand.Int() % 32 + if hasAllocBound { + l = 1 + } s := reflect.MakeSlice(v.Type(), l, l) for i := 0; i < l; i++ { err := randomizeValue(s.Index(i), fmt.Sprintf("%s/%d", datapath, i), "") @@ -157,10 +217,13 @@ func randomizeValue(v reflect.Value, datapath string, tag string) error { case reflect.Bool: v.SetBool(rand.Uint32()%2 == 0) case reflect.Map: - checkBoundsLimitingTag("map", datapath, tag) + hasAllocBound := checkBoundsLimitingTag(v, datapath, tag) mt := v.Type() v.Set(reflect.MakeMap(mt)) l := rand.Int() % 32 + if hasAllocBound { + l = 1 + } for i := 0; i < l; i++ { mk := reflect.New(mt.Key()) err := randomizeValue(mk.Elem(), fmt.Sprintf("%s/%d", datapath, i), "") From 408f34cf6e316d39bdf6da9704dc6e06a494907e Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 15 Jun 2020 15:57:12 -0400 Subject: [PATCH 053/267] Handle stopping and aborting in catchpoint catchup service (#1169) When the node is in fast catchup mode, either stopping the node or aborting the catchup does not work in fashionable manner. The stop/abort operation is expected to happen right away, whereas we had some long delays before it would complete. --- catchup/catchpointService.go | 80 +++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/catchup/catchpointService.go b/catchup/catchpointService.go index 577ee8e754..5c8313a93d 100644 --- a/catchup/catchpointService.go +++ b/catchup/catchpointService.go @@ -52,21 +52,36 @@ type CatchpointCatchupStats struct { // CatchpointCatchupService represents the catchpoint catchup service. type CatchpointCatchupService struct { - stats CatchpointCatchupStats - statsMu deadlock.Mutex - node CatchpointCatchupNodeServices - ctx context.Context - cancelCtxFunc context.CancelFunc - running sync.WaitGroup + // stats is the statistics object, updated async while downloading the ledger + stats CatchpointCatchupStats + // statsMu syncronizes access to stats, as we could attempt to update it while querying for it's current state + statsMu deadlock.Mutex + node CatchpointCatchupNodeServices + // ctx is the node cancelation context, used when the node is being stopped. + ctx context.Context + cancelCtxFunc context.CancelFunc + // running is a waitgroup counting the running goroutine(1), and allow us to exit cleanly. + running sync.WaitGroup + // ledgerAccessor is the ledger accessor used to perform ledger-level operation on the database ledgerAccessor ledger.CatchpointCatchupAccessor - stage ledger.CatchpointCatchupState - log logging.Logger - newService bool // indicates whether this service was created after the node was running ( i.e. true ) or the node just started to find that it was previously perfoming catchup - net network.GossipNode - ledger *ledger.Ledger + // stage is the current stage of the catchpoint catchup process + stage ledger.CatchpointCatchupState + // log is the logger object + log logging.Logger + // newService indicates whether this service was created after the node was running ( i.e. true ) or the node just started to find that it was previously perfoming catchup + newService bool + // net is the underlaying network module + net network.GossipNode + // ledger points to the ledger object + ledger *ledger.Ledger // lastBlockHeader is the latest block we have before going into catchpoint catchup mode. We use it to serve the node status requests instead of going to the ledger. lastBlockHeader bookkeeping.BlockHeader - config config.Local + // config is a copy of the node configuration + config config.Local + // abortCtx used as a syncronized flag to let us know when the user asked us to abort the catchpoint catchup process. note that it's not being used when we decided to abort + // the catchup due to an internal issue ( such as exceeding number of retries ) + abortCtx context.Context + abortCtxFunc context.CancelFunc } // MakeResumedCatchpointCatchupService creates a catchpoint catchup service for a node that is already in catchpoint catchup mode @@ -124,12 +139,17 @@ func MakeNewCatchpointCatchupService(catchpoint string, node CatchpointCatchupNo // Start starts the catchpoint catchup service ( continue in the process ) func (cs *CatchpointCatchupService) Start(ctx context.Context) { cs.ctx, cs.cancelCtxFunc = context.WithCancel(ctx) + cs.abortCtx, cs.abortCtxFunc = context.WithCancel(context.Background()) cs.running.Add(1) go cs.run() } // Abort aborts the catchpoint catchup process func (cs *CatchpointCatchupService) Abort() { + // In order to abort the catchpoint catchup process, we need to first set the flag of abortCtxFunc, and follow that by canceling the main context. + // The order of these calls is crucial : The various stages are blocked on the main context. When that one expires, it uses the abort context to determine + // if the cancelation meaning that we want to shut down the process, or aborting the catchpoint catchup completly. + cs.abortCtxFunc() cs.cancelCtxFunc() } @@ -140,6 +160,13 @@ func (cs *CatchpointCatchupService) Stop() { cs.cancelCtxFunc() // wait for the running goroutine to terminate. cs.running.Wait() + // call the abort context canceling, just to release it's goroutine. + cs.abortCtxFunc() +} + +// GetLatestBlockHeader returns the last block header that was available at the time the catchpoint catchup service started +func (cs *CatchpointCatchupService) GetLatestBlockHeader() bookkeeping.BlockHeader { + return cs.lastBlockHeader } // run is the main stage-swtiching background service function. It switches the current stage into the correct stage handler. @@ -244,7 +271,7 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) { err = cs.ledgerAccessor.ResetStagingBalances(cs.ctx, true) if err != nil { if err == cs.ctx.Err() { - return cs.abort(err) // we want to keep it with the context error. + return cs.stopOrAbort() } return cs.abort(fmt.Errorf("processStageLedgerDownload failed to reset staging balances : %v", err)) } @@ -253,7 +280,7 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) { break } if err == cs.ctx.Err() { - return cs.abort(err) // we want to keep it with the context error. + return cs.stopOrAbort() } if attemptsCount >= cs.config.CatchupLedgerDownloadRetryAttempts { @@ -293,7 +320,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro blk, _, client, err = fetcher.FetchBlock(cs.ctx, blockRound) if err != nil { if err == cs.ctx.Err() { - return cs.abort(err) + return cs.stopOrAbort() } if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. @@ -334,7 +361,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro err = cs.ledgerAccessor.VerifyCatchpoint(cs.ctx, blk) if err != nil { if err == cs.ctx.Err() { - return cs.abort(err) + return cs.stopOrAbort() } if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { // try again. @@ -411,7 +438,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { var client FetcherClient for attemptsCount := uint64(1); blocksFetched <= lookback; attemptsCount++ { if err := cs.ctx.Err(); err != nil { - return cs.abort(err) + return cs.stopOrAbort() } blk = nil @@ -432,7 +459,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { blk, _, client, err = fetcher.FetchBlock(cs.ctx, topBlock.Round()-basics.Round(blocksFetched)) if err != nil { if err == cs.ctx.Err() { - return cs.abort(err) + return cs.stopOrAbort() } if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { // try again. @@ -523,6 +550,15 @@ func (cs *CatchpointCatchupService) processStageSwitch() (err error) { return nil } +// stopOrAbort is called when any of the stage processing function sees that cs.ctx has been canceled. It can be +// due to the end user attempting to abort the current catchpoint catchup operation or due to a node shutdown. +func (cs *CatchpointCatchupService) stopOrAbort() error { + if cs.abortCtx.Err() == context.Canceled { + return cs.abort(context.Canceled) + } + return nil +} + // abort aborts the current catchpoint catchup process, reverting to node to standard operation. func (cs *CatchpointCatchupService) abort(originatingErr error) error { outError := originatingErr @@ -532,7 +568,8 @@ func (cs *CatchpointCatchupService) abort(originatingErr error) error { } cs.updateNodeCatchupMode(false) // we want to abort the catchpoint catchup process, and the node already reverted to normal operation. - // at this point, all we need to do is to abort the run function. + // as part of the returning to normal operation, we've re-created our context. This context need to be + // canceled so that when we go back to run(), we would exit from there right away. cs.cancelCtxFunc() return outError } @@ -575,8 +612,3 @@ func (cs *CatchpointCatchupService) updateBlockRetrievalStatistics(aquiredBlocks cs.stats.AcquiredBlocks = uint64(int64(cs.stats.AcquiredBlocks) + aquiredBlocksDelta) cs.stats.VerifiedBlocks = uint64(int64(cs.stats.VerifiedBlocks) + verifiedBlocksDelta) } - -// GetLatestBlockHeader returns the last block header that was available at the time the catchpoint catchup service started -func (cs *CatchpointCatchupService) GetLatestBlockHeader() bookkeeping.BlockHeader { - return cs.lastBlockHeader -} From 8827e66e65c872ba27aa8474ed41d0cc82ba6704 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 16 Jun 2020 11:02:32 -0400 Subject: [PATCH 054/267] Increase travis builtin vm.max_map_count (#1175) Try to increase max_map_count to reduce sporadic travis failures. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9363b6ada9..34d016263a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,6 +36,8 @@ jobs: - stage: build_pr os: linux name: Ubuntu AMD64 Build + before_script: + - sudo sysctl -w vm.max_map_count=262144 script: - scripts/travis/build_test.sh - # same stage, parallel job @@ -83,6 +85,8 @@ jobs: - stage: build_release os: linux name: Ubuntu AMD64 Build + before_script: + - sudo sysctl -w vm.max_map_count=262144 script: - ./scripts/travis/build_test.sh - # same stage, parallel job From d7a82139dbf580592fa760178c98084cee1bdfe2 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 16 Jun 2020 16:27:58 -0400 Subject: [PATCH 055/267] rollback .travis.yml by removing the max_map_count change. (#1176) Adding vm.max_map_count to travis did not helped to fix the observed issue #1175 was intended to solve. This PR rolls back the change. --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 34d016263a..9363b6ada9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,8 +36,6 @@ jobs: - stage: build_pr os: linux name: Ubuntu AMD64 Build - before_script: - - sudo sysctl -w vm.max_map_count=262144 script: - scripts/travis/build_test.sh - # same stage, parallel job @@ -85,8 +83,6 @@ jobs: - stage: build_release os: linux name: Ubuntu AMD64 Build - before_script: - - sudo sysctl -w vm.max_map_count=262144 script: - ./scripts/travis/build_test.sh - # same stage, parallel job From abab5570e6fa46833b7e49bf8cca803c0a2e3be3 Mon Sep 17 00:00:00 2001 From: btoll Date: Tue, 16 Jun 2020 20:05:33 -0400 Subject: [PATCH 056/267] Fix content of hashes file(s) (#1173) This removes "./" from the beginning of some lines in the hashes files. --- scripts/release/mule/sign/sign.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/release/mule/sign/sign.sh b/scripts/release/mule/sign/sign.sh index f9dec69b27..ddf73836f2 100755 --- a/scripts/release/mule/sign/sign.sh +++ b/scripts/release/mule/sign/sign.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +# shellcheck disable=2035 set -exo pipefail @@ -51,9 +52,9 @@ make_hashes () { rm -f "$HASHFILE"* { - md5sum ./*"$VERSION"*."$PACKAGE_TYPE" ; - shasum -a 256 ./*"$VERSION"*."$PACKAGE_TYPE" ; - shasum -a 512 ./*"$VERSION"*."$PACKAGE_TYPE" ; + md5sum *"$VERSION"*."$PACKAGE_TYPE" ; + shasum -a 256 *"$VERSION"*."$PACKAGE_TYPE" ; + shasum -a 512 *"$VERSION"*."$PACKAGE_TYPE" ; } >> "$HASHFILE" gpg -u "$SIGNING_KEY_ADDR" --detach-sign "$HASHFILE" From 612cbe55eafbbc870d1909c6b40ca1d0dfb5bee6 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Tue, 16 Jun 2020 20:12:24 -0400 Subject: [PATCH 057/267] Fix typo in goal/clerk (#1178) Fix typo in goal clerk option disassesmble --- cmd/goal/clerk.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index fabb2e8156..e3e09b1ba1 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -56,7 +56,7 @@ var ( signProgram bool programSource string argB64Strings []string - disassesmble bool + disassemble bool progByteFile string logicSigFile string timeStamp int64 @@ -128,7 +128,7 @@ func init() { splitCmd.MarkFlagRequired("infile") splitCmd.MarkFlagRequired("outfile") - compileCmd.Flags().BoolVarP(&disassesmble, "disassemble", "D", false, "disassemble a compiled program") + compileCmd.Flags().BoolVarP(&disassemble, "disassemble", "D", false, "disassemble a compiled program") compileCmd.Flags().BoolVarP(&noProgramOutput, "no-out", "n", false, "don't write contract program binary") compileCmd.Flags().BoolVarP(&signProgram, "sign", "s", false, "sign program, output is a binary signed LogicSig record") compileCmd.Flags().StringVarP(&outFilename, "outfile", "o", "", "Filename to write program bytes or signed LogicSig to") @@ -837,7 +837,7 @@ var compileCmd = &cobra.Command{ Long: "Reads a TEAL contract program and compiles it to binary output and contract address.", Run: func(cmd *cobra.Command, args []string) { for _, fname := range args { - if disassesmble { + if disassemble { disassembleFile(fname, outFilename) continue } From 196c7937b729287142fa5a3d63b574266454207a Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 17 Jun 2020 15:12:08 -0400 Subject: [PATCH 058/267] Regenerate routes to allow 'pretty' parameter. (#1153) Regenerate routes to allow 'pretty' parameter. --- daemon/algod/api/Makefile | 2 +- .../api/server/v2/generated/private/routes.go | 10 ++++-- .../algod/api/server/v2/generated/routes.go | 32 +++++++++++++++---- go.mod | 14 ++++---- go.sum | 16 ++++++++++ 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/daemon/algod/api/Makefile b/daemon/algod/api/Makefile index 6df923f29a..0512d68827 100644 --- a/daemon/algod/api/Makefile +++ b/daemon/algod/api/Makefile @@ -23,6 +23,6 @@ algod.oas3.yml: .3tmp.json curl -s -X POST "https://converter.swagger.io/api/convert" -H "accept: application/json" -H "Content-Type: application/json" -d @./algod.oas2.json -o .3tmp.json oapi-codegen: .PHONY - GO111MODULE=on go get -u "github.com/algorand/oapi-codegen/...@v1.3.5-algorand4" + GO111MODULE=on go get -u "github.com/algorand/oapi-codegen/...@v1.3.5-algorand5" .PHONY: diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index c00d8be6b7..578938fd62 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -39,7 +39,9 @@ type ServerInterfaceWrapper struct { // AbortCatchup converts echo context to params. func (w *ServerInterfaceWrapper) AbortCatchup(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -67,7 +69,9 @@ func (w *ServerInterfaceWrapper) AbortCatchup(ctx echo.Context) error { // StartCatchup converts echo context to params. func (w *ServerInterfaceWrapper) StartCatchup(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -96,6 +100,7 @@ func (w *ServerInterfaceWrapper) StartCatchup(ctx echo.Context) error { func (w *ServerInterfaceWrapper) RegisterParticipationKeys(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "fee": true, "key-dilution": true, "round-last-valid": true, @@ -171,6 +176,7 @@ func (w *ServerInterfaceWrapper) RegisterParticipationKeys(ctx echo.Context) err func (w *ServerInterfaceWrapper) ShutdownNode(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "timeout": true, } diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index cfe5f63fde..7a0d295c58 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -60,7 +60,9 @@ type ServerInterfaceWrapper struct { // AccountInformation converts echo context to params. func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -89,6 +91,7 @@ func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetPendingTransactionsByAddress(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "max": true, "format": true, } @@ -142,6 +145,7 @@ func (w *ServerInterfaceWrapper) GetPendingTransactionsByAddress(ctx echo.Contex func (w *ServerInterfaceWrapper) GetBlock(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "format": true, } @@ -183,7 +187,9 @@ func (w *ServerInterfaceWrapper) GetBlock(ctx echo.Context) error { // GetSupply converts echo context to params. func (w *ServerInterfaceWrapper) GetSupply(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -204,7 +210,9 @@ func (w *ServerInterfaceWrapper) GetSupply(ctx echo.Context) error { // GetStatus converts echo context to params. func (w *ServerInterfaceWrapper) GetStatus(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -225,7 +233,9 @@ func (w *ServerInterfaceWrapper) GetStatus(ctx echo.Context) error { // WaitForBlock converts echo context to params. func (w *ServerInterfaceWrapper) WaitForBlock(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -253,7 +263,9 @@ func (w *ServerInterfaceWrapper) WaitForBlock(ctx echo.Context) error { // TealCompile converts echo context to params. func (w *ServerInterfaceWrapper) TealCompile(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -274,7 +286,9 @@ func (w *ServerInterfaceWrapper) TealCompile(ctx echo.Context) error { // RawTransaction converts echo context to params. func (w *ServerInterfaceWrapper) RawTransaction(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -295,7 +309,9 @@ func (w *ServerInterfaceWrapper) RawTransaction(ctx echo.Context) error { // TransactionParams converts echo context to params. func (w *ServerInterfaceWrapper) TransactionParams(ctx echo.Context) error { - validQueryParams := map[string]bool{} + validQueryParams := map[string]bool{ + "pretty": true, + } // Check for unknown query parameters. for name, _ := range ctx.QueryParams() { @@ -317,6 +333,7 @@ func (w *ServerInterfaceWrapper) TransactionParams(ctx echo.Context) error { func (w *ServerInterfaceWrapper) GetPendingTransactions(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "max": true, "format": true, } @@ -363,6 +380,7 @@ func (w *ServerInterfaceWrapper) GetPendingTransactions(ctx echo.Context) error func (w *ServerInterfaceWrapper) PendingTransactionInformation(ctx echo.Context) error { validQueryParams := map[string]bool{ + "pretty": true, "format": true, } diff --git a/go.mod b/go.mod index b76365f20a..95e8290a76 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 github.com/algorand/msgp v1.1.45 - github.com/algorand/oapi-codegen v1.3.5-algorand4 + github.com/algorand/oapi-codegen v1.3.5-algorand5 github.com/algorand/websocket v1.4.1 github.com/aws/aws-sdk-go v1.16.5 github.com/cpuguy83/go-md2man v1.0.8 // indirect @@ -15,7 +15,8 @@ require ( github.com/fatih/color v1.7.0 github.com/fortytw2/leaktest v1.3.0 // indirect github.com/gen2brain/beeep v0.0.0-20180718162406-4e430518395f - github.com/getkin/kin-openapi v0.8.0 + github.com/getkin/kin-openapi v0.9.0 + github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f // indirect github.com/gofrs/flock v0.7.0 github.com/google/go-querystring v1.0.0 @@ -30,6 +31,7 @@ require ( github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2 github.com/labstack/echo/v4 v4.1.16 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 // indirect + github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6 // indirect github.com/mattn/go-sqlite3 v1.10.0 github.com/miekg/dns v1.1.27 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect @@ -43,11 +45,11 @@ require ( github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.5.1 - golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 + golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 golang.org/x/mod v0.3.0 // indirect - golang.org/x/net v0.0.0-20200513185701-a91f0712d120 - golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 - golang.org/x/tools v0.0.0-20200518195306-39aadb5b76f2 // indirect + golang.org/x/net v0.0.0-20200602114024-627f9648deb9 + golang.org/x/sys v0.0.0-20200610111108-226ff32320da + golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae // indirect google.golang.org/appengine v1.6.1 // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect diff --git a/go.sum b/go.sum index e7c015b44c..a2e2bb9236 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/algorand/msgp v1.1.45 h1:uLEPvg0BfTrb2JjBcXexON8il4vv0EsD7HYFaA7e5jg= github.com/algorand/msgp v1.1.45/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= github.com/algorand/oapi-codegen v1.3.5-algorand4 h1:skjv/lhlWAGnQPHQ9qdzTE5Bk9W555EKrh1bSul0JJs= github.com/algorand/oapi-codegen v1.3.5-algorand4/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= +github.com/algorand/oapi-codegen v1.3.5-algorand5 h1:y576Ca2/guQddQrQA7dtL5KcOx5xQgPeIupiuFMGyCI= +github.com/algorand/oapi-codegen v1.3.5-algorand5/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= github.com/algorand/websocket v1.4.1 h1:FPoNHI8i2VZWZzhCscY8JTzsAE7Vv73753cMbzb3udk= github.com/algorand/websocket v1.4.1/go.mod h1:0nFSn+xppw/GZS9hgWPS3b8/4FcA3Pj7XQxm+wqHGx8= github.com/aws/aws-sdk-go v1.16.5 h1:NVxzZXIuwX828VcJrpNxxWjur1tlOBISdMdDdHIKHcc= @@ -34,10 +36,14 @@ github.com/gen2brain/beeep v0.0.0-20180718162406-4e430518395f/go.mod h1:GprdPCZg github.com/getkin/kin-openapi v0.3.1/go.mod h1:W8dhxZgpE84ciM+VIItFqkmZ4eHtuomrdIHtASQIqi0= github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE= github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= +github.com/getkin/kin-openapi v0.9.0 h1:/vaUQkiOR+vfFO3oilZentZTfAhz7OzXPhLdNas4q4w= +github.com/getkin/kin-openapi v0.9.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4= github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f h1:zlOR3rOlPAVvtfuxGKoghCmop5B0TRyu/ZieziZuGiM= github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= @@ -77,6 +83,8 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 h1:p8zN0Xu28xyEkPpqLbFXAnjdgBVvTJCpfOtoDf/+/RQ= github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6 h1:Cx1ZvZ3SQTli1nKee9qvJ/NJP3vt11s+ilM7NF3QSL8= +github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -133,6 +141,8 @@ golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROx golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= +golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -152,6 +162,8 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -172,6 +184,8 @@ golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3 golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= +golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -186,6 +200,8 @@ golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vv golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200518195306-39aadb5b76f2 h1:x5byaLtwftlfxkjQwPYSwshF9F3HE8iVApWvVu60NtY= golang.org/x/tools v0.0.0-20200518195306-39aadb5b76f2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae h1:Tzc/wv256GkBNOkrWbHBeV3T08mrVevgo/q5KSGltr0= +golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From 6e6a19ae911dc3a598cadda0695127993e7ac654 Mon Sep 17 00:00:00 2001 From: btoll Date: Wed, 17 Jun 2020 20:30:33 -0400 Subject: [PATCH 059/267] Fix deb package name (#1174) This adds the CHANNEL and the OS_TYPE and arranges the bits in the proper order. --- scripts/release/mule/package/deb/package.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/release/mule/package/deb/package.sh b/scripts/release/mule/package/deb/package.sh index fde509261d..83790d12b4 100755 --- a/scripts/release/mule/package/deb/package.sh +++ b/scripts/release/mule/package/deb/package.sh @@ -117,7 +117,7 @@ sed 's/^$/./g' < COPYING | sed 's/^/ /g' >> "${PKG_ROOT}/DEBIAN/copyright" mkdir -p "${PKG_ROOT}/usr/share/doc/algorand" cp -p "${PKG_ROOT}/DEBIAN/copyright" "${PKG_ROOT}/usr/share/doc/algorand/copyright" -OUTPUT="$OUTDIR/algorand_${VER}_${ARCH}.deb" +OUTPUT="$OUTDIR/algorand_${CHANNEL}_${OS_TYPE}-${ARCH}_${VER}.deb" dpkg-deb --build "${PKG_ROOT}" "${OUTPUT}" echo From 171d3fe977e2818901114c37677996c23e5c6394 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 18 Jun 2020 11:01:54 -0400 Subject: [PATCH 060/267] Reduce parallel testings. (#1177) Reducing the amount of parallel testing on travis is likely to reduce the memory demand stress. On travis machines, where memory is limited, this could help us avoid OOM cases. --- scripts/travis/run_tests.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/travis/run_tests.sh b/scripts/travis/run_tests.sh index 24b77081e1..1322e2fc0b 100755 --- a/scripts/travis/run_tests.sh +++ b/scripts/travis/run_tests.sh @@ -16,10 +16,12 @@ if [ "${BUILD_TYPE}" = "integration" ]; then ./test/scripts/run_integration_tests.sh elif [ "${TRAVIS_EVENT_TYPE}" = "cron" ] || [[ "${TRAVIS_BRANCH}" =~ ^rel/ ]]; then if [[ "${OS}" != "darwin" ]]; then - make fulltest -j4 + make fulltest -j2 fi else if [[ "${OS}" != "darwin" ]]; then - make shorttest -j4 + # setting it to 1 disable parallel making. This is done specicifically for travis, as travis seems to + # have memory limitations and setting this to 1 could reduce the likelihood of hitting these. + make shorttest -j1 fi fi From 0b3c3abbfcd2a128306453b0e664cb4d4f0aa5bf Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Thu, 18 Jun 2020 11:05:47 -0400 Subject: [PATCH 061/267] Unify txFilename and outFilename vars usage in goal (#1179) txFilename and outFilename variables in goal now are used consistently, one for reading and another for writing out. --- cmd/goal/asset.go | 30 +++++++++++++++--------------- cmd/goal/clerk.go | 12 ++++++------ cmd/goal/multisig.go | 6 +++--- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/cmd/goal/asset.go b/cmd/goal/asset.go index 3d9de1fe95..59b1ee8f81 100644 --- a/cmd/goal/asset.go +++ b/cmd/goal/asset.go @@ -67,7 +67,7 @@ func init() { createAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") createAssetCmd.Flags().StringVar(&assetURL, "asseturl", "", "URL where user can access more information about the asset (max 32 bytes)") createAssetCmd.Flags().StringVar(&assetMetadataHashBase64, "assetmetadatab64", "", "base-64 encoded 32-byte commitment to asset metadata") - createAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") + createAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") createAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") createAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") createAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") @@ -84,7 +84,7 @@ func init() { destroyAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") destroyAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") destroyAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - destroyAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") + destroyAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") destroyAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") destroyAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") destroyAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") @@ -103,7 +103,7 @@ func init() { configAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") configAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") configAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - configAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") + configAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") configAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") configAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") configAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") @@ -123,7 +123,7 @@ func init() { sendAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") sendAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") sendAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - sendAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") + sendAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") sendAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") sendAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") sendAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") @@ -142,7 +142,7 @@ func init() { freezeAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") freezeAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") freezeAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - freezeAssetCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Write transaction to this file") + freezeAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") freezeAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") freezeAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") freezeAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") @@ -247,7 +247,7 @@ var createAssetCmd = &cobra.Command{ reportErrorf("Cannot construct transaction: %s", err) } - if txFilename == "" { + if outFilename == "" { wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) if err != nil { @@ -277,7 +277,7 @@ var createAssetCmd = &cobra.Command{ } } } else { - err = writeTxnToFile(client, sign, dataDir, walletName, tx, txFilename) + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) if err != nil { reportErrorf(err.Error()) } @@ -323,7 +323,7 @@ var destroyAssetCmd = &cobra.Command{ reportErrorf("Cannot construct transaction: %s", err) } - if txFilename == "" { + if outFilename == "" { wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) if err != nil { @@ -345,7 +345,7 @@ var destroyAssetCmd = &cobra.Command{ } } } else { - err = writeTxnToFile(client, sign, dataDir, walletName, tx, txFilename) + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) if err != nil { reportErrorf(err.Error()) } @@ -412,7 +412,7 @@ var configAssetCmd = &cobra.Command{ reportErrorf("Cannot construct transaction: %s", err) } - if txFilename == "" { + if outFilename == "" { wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) if err != nil { @@ -434,7 +434,7 @@ var configAssetCmd = &cobra.Command{ } } } else { - err = writeTxnToFile(client, sign, dataDir, walletName, tx, txFilename) + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) if err != nil { reportErrorf(err.Error()) } @@ -493,7 +493,7 @@ var sendAssetCmd = &cobra.Command{ reportErrorf("Cannot construct transaction: %s", err) } - if txFilename == "" { + if outFilename == "" { wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) if err != nil { @@ -515,7 +515,7 @@ var sendAssetCmd = &cobra.Command{ } } } else { - err = writeTxnToFile(client, sign, dataDir, walletName, tx, txFilename) + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) if err != nil { reportErrorf(err.Error()) } @@ -558,7 +558,7 @@ var freezeAssetCmd = &cobra.Command{ reportErrorf("Cannot construct transaction: %s", err) } - if txFilename == "" { + if outFilename == "" { wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) if err != nil { @@ -580,7 +580,7 @@ var freezeAssetCmd = &cobra.Command{ } } } else { - err = writeTxnToFile(client, sign, dataDir, walletName, tx, txFilename) + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) if err != nil { reportErrorf(err.Error()) } diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index e3e09b1ba1..97870af933 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -89,7 +89,7 @@ func init() { sendCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") sendCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") sendCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - sendCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Dump an unsigned tx to the given file. In order to dump a signed transaction, pass -s") + sendCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Dump an unsigned tx to the given file. In order to dump a signed transaction, pass -s") sendCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") sendCmd.Flags().StringVarP(&closeToAddress, "close-to", "c", "", "Close account and send remainder to this address") sendCmd.Flags().StringVar(&rekeyToAddress, "rekey-to", "", "Rekey account to the given authorization address. (Future transactions from this account will need to be signed with the new key.)") @@ -273,7 +273,7 @@ var sendCmd = &cobra.Command{ Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { // -s is invalid without -o - if txFilename == "" && sign { + if outFilename == "" && sign { reportErrorln(soFlagError) } @@ -370,7 +370,7 @@ var sendCmd = &cobra.Command{ } err = verify.LogicSigSanityCheck(&uncheckedTxn, &verify.Context{Params: verify.Params{CurrProto: proto}}) if err != nil { - reportErrorf("%s: txn[0] error %s", txFilename, err) + reportErrorf("%s: txn[0] error %s", outFilename, err) } stx = uncheckedTxn } else if program != nil { @@ -382,14 +382,14 @@ var sendCmd = &cobra.Command{ }, } } else { - signTx := sign || (txFilename == "") + signTx := sign || (outFilename == "") stx, err = createSignedTransaction(client, signTx, dataDir, walletName, payment) if err != nil { reportErrorf(errorSigningTX, err) } } - if txFilename == "" { + if outFilename == "" { // Broadcast the tx txid, err := client.BroadcastTransaction(stx) @@ -410,7 +410,7 @@ var sendCmd = &cobra.Command{ } } } else { - err = writeFile(txFilename, protocol.Encode(&stx), 0600) + err = writeFile(outFilename, protocol.Encode(&stx), 0600) if err != nil { reportErrorf(err.Error()) } diff --git a/cmd/goal/multisig.go b/cmd/goal/multisig.go index 5198e549ad..efd9b22454 100644 --- a/cmd/goal/multisig.go +++ b/cmd/goal/multisig.go @@ -56,7 +56,7 @@ func init() { signProgramCmd.Flags().StringVarP(&outFilename, "lsig-out", "o", "", "File to write partial Lsig to") signProgramCmd.MarkFlagRequired("address") - mergeSigCmd.Flags().StringVarP(&txFilename, "out", "o", "", "Output file for merged transactions") + mergeSigCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Output file for merged transactions") mergeSigCmd.MarkFlagRequired("out") } @@ -287,9 +287,9 @@ var mergeSigCmd = &cobra.Command{ mergedData = append(mergedData, protocol.Encode(&txn)...) } - err := writeFile(txFilename, mergedData, 0600) + err := writeFile(outFilename, mergedData, 0600) if err != nil { - reportErrorf(fileWriteError, txFilename, err) + reportErrorf(fileWriteError, outFilename, err) } }, } From 2da3dcf742dee5005477d55c80e30492cdf71366 Mon Sep 17 00:00:00 2001 From: wjurayj-algo <65556361+wjurayj-algo@users.noreply.github.com> Date: Thu, 18 Jun 2020 14:47:04 -0400 Subject: [PATCH 062/267] Algod telemetry config location logging (#1154) Removed non-front facing flag, externalHostName, from diagcfg metric command. Added logging for telemetry config file location in algod and algoh startup processes. --- cmd/algod/main.go | 1 + cmd/algoh/main.go | 1 + cmd/diagcfg/messages.go | 4 - cmd/diagcfg/metric.go | 66 +------- .../cli/algod/expect/algodExpectCommon.exp | 49 ++++++ .../expect/algodTelemetryLocationTest.exp | 37 ++++ .../cli/algod/expect/algod_expect_test.go | 158 ++++++++++++++++++ 7 files changed, 250 insertions(+), 66 deletions(-) create mode 100644 test/e2e-go/cli/algod/expect/algodExpectCommon.exp create mode 100644 test/e2e-go/cli/algod/expect/algodTelemetryLocationTest.exp create mode 100644 test/e2e-go/cli/algod/expect/algod_expect_test.go diff --git a/cmd/algod/main.go b/cmd/algod/main.go index ab8d2fb7d7..4e8cfd832c 100644 --- a/cmd/algod/main.go +++ b/cmd/algod/main.go @@ -179,6 +179,7 @@ func main() { fmt.Fprintf(os.Stderr, "Permission error on accessing telemetry config: %v", err) os.Exit(1) } + fmt.Fprintf(os.Stdout, "Telemetry configured from '%s'\n", telemetryConfig.FilePath) telemetryConfig.SendToLog = telemetryConfig.SendToLog || cfg.TelemetryToLog diff --git a/cmd/algoh/main.go b/cmd/algoh/main.go index 22cdf3189f..04bcdcb4f3 100644 --- a/cmd/algoh/main.go +++ b/cmd/algoh/main.go @@ -291,6 +291,7 @@ func initTelemetry(genesis bookkeeping.Genesis, log logging.Logger, dataDirector fmt.Fprintln(os.Stdout, "error loading telemetry config", err) return } + fmt.Fprintf(os.Stdout, "algoh telemetry configured from '%s'\n", telemetryConfig.FilePath) // Apply telemetry override. telemetryConfig.Enable = logging.TelemetryOverride(*telemetryOverride) diff --git a/cmd/diagcfg/messages.go b/cmd/diagcfg/messages.go index 16e2d4b255..8fbbbffe4f 100644 --- a/cmd/diagcfg/messages.go +++ b/cmd/diagcfg/messages.go @@ -25,10 +25,6 @@ const ( metricConfigReadingFailed = "Failed to read configuration file : %s\n" metricReportingStatus = "Metric reporting is %s\n" metricSaveConfigFailed = "Metric configuration file could not be saved : %s\n" - metricFailedSetDNS = "Failed to store DNS : %s\n" - metricCloudflareCredentialMissing = "Cloudflare credentials are missing; Please configure environment variables CLOUDFLARE_ZONE_ID, CLOUDFLARE_EMAIL and CLOUDFLARE_AUTH_KEY" - metricNoExternalHostAndFailedAutoDetect = "No external host name was provided, and auto-detecting external IP address failed : %v\n" - metricNoExternalHostUsingAutoDetectedIP = "No external host name was provided; auto-detecting external IP address = %s\n" metricDataDirectoryEmpty = "no data directory was specified. Please use either -d or set environment variable ALGORAND_DATA" telemetryConfigReadError = "Could not read telemetry config: %s\n" diff --git a/cmd/diagcfg/metric.go b/cmd/diagcfg/metric.go index 80d1b597f6..53d08eaa21 100644 --- a/cmd/diagcfg/metric.go +++ b/cmd/diagcfg/metric.go @@ -17,23 +17,17 @@ package main import ( - "context" "fmt" "os" "path/filepath" - "strconv" - "strings" "github.com/spf13/cobra" "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/tools/network" - "github.com/algorand/go-algorand/tools/network/cloudflare" ) var ( - dataDir string - externalHostName string + dataDir string ) func init() { @@ -44,9 +38,6 @@ func init() { // override the data directory, if provided in the command. metricCmd.PersistentFlags().StringVarP(&dataDir, "dataDir", "d", os.ExpandEnv("$ALGORAND_DATA"), "Data directory") - metricEnableCmd.Flags().StringVarP(&externalHostName, "externalHostName", "e", "", "External host name, such as relay-us-ea-3.algodev.network; will default to external IP Address if not specified") - metricDisableCmd.Flags().StringVarP(&externalHostName, "externalHostName", "e", "", "External host name, such as relay-us-ea-3.algodev.network; will default to external IP Address if not specified") - } var metricCmd = &cobra.Command{ @@ -80,7 +71,7 @@ var metricStatusCmd = &cobra.Command{ } var metricEnableCmd = &cobra.Command{ - Use: "enable -d dataDir -e externalHostName", + Use: "enable -d dataDir", Short: "Enable metric collection on node", Long: `Enable metric collection on node`, Run: func(cmd *cobra.Command, args []string) { @@ -89,7 +80,7 @@ var metricEnableCmd = &cobra.Command{ } var metricDisableCmd = &cobra.Command{ - Use: "disable -d dataDir -e externalHostName", + Use: "disable -d dataDir", Short: "Disable metric collection on node", Long: `Disable metric collection on node`, Run: func(cmd *cobra.Command, args []string) { @@ -113,31 +104,7 @@ func metricEnableDisable(enable bool) { if err != nil { fmt.Printf(metricSaveConfigFailed, fmt.Sprintf("%v", err)) } - if !updateExternalHostName() { - return - } - - domainName := strings.Replace(localConfig.DNSBootstrapID, ".", "", 1) - cfZoneID, cfEmail, cfKey, err := getClouldflareCredentials() - if err != nil { - fmt.Printf(metricFailedSetDNS, fmt.Sprintf("%v", err)) - return - } - cloudflareDNS := cloudflare.NewDNS(cfZoneID, cfEmail, cfKey) - if enable { - port, err := strconv.ParseInt(strings.Split(localConfig.NodeExporterListenAddress, ":")[1], 10, 64) - if err != nil { - fmt.Printf(metricFailedSetDNS, fmt.Sprintf("%v", err)) - return - } - err = cloudflareDNS.SetSRVRecord(context.Background(), domainName, externalHostName, 1 /*ttl*/, 1 /*priority*/, uint(port) /*port*/, "_metrics", "_tcp", 1 /*weight*/) - } else { - err = cloudflareDNS.ClearSRVRecord(context.Background(), domainName, externalHostName, "_metrics", "_tcp") - } - - if err != nil { - fmt.Printf(metricFailedSetDNS, fmt.Sprintf("%v", err)) - } + return } func getConfigFilePath() (string, error) { @@ -162,28 +129,3 @@ func getConfigFilePath() (string, error) { } return configFilePath, nil } - -func updateExternalHostName() bool { - if externalHostName == "" { - ipList, err := network.GetExternalIPAddress(context.Background()) - if err == nil && len(ipList) > 0 { - externalHostName = ipList[0].String() - } else { - fmt.Printf(metricNoExternalHostAndFailedAutoDetect, err) - return false - } - fmt.Printf(metricNoExternalHostUsingAutoDetectedIP, externalHostName) - } - return true -} - -func getClouldflareCredentials() (string, string, string, error) { - zoneID := os.Getenv("CLOUDFLARE_ZONE_ID") - email := os.Getenv("CLOUDFLARE_EMAIL") - authKey := os.Getenv("CLOUDFLARE_AUTH_KEY") - if zoneID == "" || email == "" || authKey == "" { - fmt.Println(metricCloudflareCredentialMissing) - return "", "", "", fmt.Errorf("%s", metricCloudflareCredentialMissing) - } - return zoneID, email, authKey, nil -} diff --git a/test/e2e-go/cli/algod/expect/algodExpectCommon.exp b/test/e2e-go/cli/algod/expect/algodExpectCommon.exp new file mode 100644 index 0000000000..03e898032b --- /dev/null +++ b/test/e2e-go/cli/algod/expect/algodExpectCommon.exp @@ -0,0 +1,49 @@ +# Algod Expect Utility Package +namespace eval ::Algod { + + # Export Procedures + namespace export Info + namespace export ReadTelemetry + + + # My Variables + set version 1.0 + set description "Algod Expect Package" + + # Variable for the path of the script + variable home [file join [pwd] [file dirname [info script]]] +} + +# Definition of the procedure MyProcedure +proc ::Algod::Info {} { + puts Algod::description +} + +package provide Algod $Algod::version +package require Tcl 8.0 + + +# Start a node, and confirm it pulls telemetry config from the correct location +proc ::Algod::ReadTelemetry { TEST_ALGO_DIR } { + set timeout 5 + + if { [catch { + puts "algod start with $TEST_ALGO_DIR" + spawn $env(GOPATH)/bin/algod -d $TEST_ALGO_DIR -g ../../../../../gen/testnet/genesis.json + expect { + "^Telemetry configured from '$::env(HOME)/.algorand/logging.config'*" {puts "Telemetry config correctly pulled from global location"; send '\003'; close} + "^Telemetry configured from *" {puts "Telemetry config pulled from unexpected location"; send '\003'; close; exit 1} + timeout {puts "timeout occurred waiting for telemetry log"; send '\003'; close; exit 1} + } + + exec cp $::env(HOME)/.algorand/logging.config $TEST_ALGO_DIR + spawn $env(GOPATH)/bin/algod -d $TEST_ALGO_DIR -g ../../../../../gen/testnet/genesis.json + expect { + timeout {send '\003'; close; puts "timeout occurred waiting for telemetry log"; exit 1} + "^Telemetry configured from '$TEST_ALGO_DIR/logging.config'*" {puts "Telemetry config correctly pulled from local location"; send '\003'; close} + "^Telemetry configured from *" {puts "Telemetry config pulled from unexpected location, not $TEST_ALGO_DIR/logging.config"; send '\003'; close; exit 1} + } + } EXCEPTION] } { + puts "ERROR in ReadTelemetry: $EXCEPTION" + } +} diff --git a/test/e2e-go/cli/algod/expect/algodTelemetryLocationTest.exp b/test/e2e-go/cli/algod/expect/algodTelemetryLocationTest.exp new file mode 100644 index 0000000000..d25628338a --- /dev/null +++ b/test/e2e-go/cli/algod/expect/algodTelemetryLocationTest.exp @@ -0,0 +1,37 @@ +#!/usr/bin/expect -f +set err 0 +log_user 1 + + + +if { [catch { + source algodExpectCommon.exp + + set TEST_ALGO_DIR [lindex $argv 0] + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary + + #allows script to be run outside of go context + exec mkdir -p $TEST_PRIMARY_NODE_DIR + + exec goal node stop -d $TEST_PRIMARY_NODE_DIR + + exec rm -f $TEST_PRIMARY_NODE_DIR/logging.config + + ::Algod::ReadTelemetry $TEST_PRIMARY_NODE_DIR + + + exec rm -d -r -f $TEST_ALGO_DIR + puts "Basic Algod Test Successful" + exit 0 +} EXCEPTION ] } { + puts "ERROR in algod test: $EXCEPTION" + + exec rm -d -r -f $TEST_ALGO_DIR + exit 1 +} diff --git a/test/e2e-go/cli/algod/expect/algod_expect_test.go b/test/e2e-go/cli/algod/expect/algod_expect_test.go new file mode 100644 index 0000000000..0ffa2548b5 --- /dev/null +++ b/test/e2e-go/cli/algod/expect/algod_expect_test.go @@ -0,0 +1,158 @@ +// 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 . + +package expect + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +type algodExpectFixture struct { + testDir string + testDataDir string + testDirTmp bool + t *testing.T + testFilter string +} + +func (f *algodExpectFixture) initialize(t *testing.T) (err error) { + f.t = t + f.testDir = os.Getenv("TESTDIR") + if f.testDir == "" { + f.testDir, _ = ioutil.TempDir("", "tmp") + f.testDir = filepath.Join(f.testDir, "expect") + err = os.MkdirAll(f.testDir, 0755) + if err != nil { + f.t.Errorf("error creating test dir %s, with error %v", f.testDir, err) + return + } + f.testDirTmp = true + } + f.testDataDir = os.Getenv("TESTDATADIR") + if f.testDataDir == "" { + f.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") + } + + f.testFilter = os.Getenv("TESTFILTER") + if f.testFilter == "" { + f.testFilter = ".*" + } + return +} + +func (f *algodExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { + testName = strings.Replace(testName, ".exp", "", -1) + workingDir = filepath.Join(f.testDir, testName) + err = os.Mkdir(workingDir, 0755) + if err != nil { + f.t.Errorf("error creating test dir %s, with error %v", workingDir, err) + return + } + algoDir = filepath.Join(workingDir, "algod") + err = os.Mkdir(algoDir, 0755) + if err != nil { + f.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) + return + } + return +} + +func (f *algodExpectFixture) removeTestDir(workingDir string) (err error) { + err = os.RemoveAll(workingDir) + if err != nil { + f.t.Errorf("error removing test dir %s, with error %v", workingDir, err) + return + } + return +} + +// TestAlgodWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algod/expect directory +func TestAlgodWithExpect(t *testing.T) { + var f algodExpectFixture + var execCommand = exec.Command + expectFiles := make(map[string]string) // map expect test to full file name. + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(info.Name(), "Test.exp") { + expectFiles[info.Name()] = path + } + return nil + }) + require.NoError(t, err) + err = f.initialize(t) + require.NoError(t, err) + + for testName := range expectFiles { + if match, _ := regexp.MatchString(f.testFilter, testName); match { + t.Run(testName, func(t *testing.T) { + workingDir, algoDir, err := f.getTestDir(testName) + require.NoError(t, err) + t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, f.testDataDir) + cmd := execCommand("expect", testName, algoDir, f.testDataDir) + var outBuf bytes.Buffer + cmd.Stdout = &outBuf + + // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor + // attaches goroutine reading that blocks on io.Copy from stderr. + // Cmd::CombinedOutput sets stderr to stdout and also blocks. + // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. + // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. + // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. + // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. + errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) + if err != nil { + t.Logf("failed opening stderr temp file: %s\n", err.Error()) + t.Fail() + } + defer errFile.Close() // Close might error but we Sync it before leaving the scope + cmd.Stderr = errFile + + err = cmd.Run() + if err != nil { + var stderr string + var ferr error + if ferr = errFile.Sync(); ferr == nil { + if _, ferr = errFile.Seek(0, 0); ferr == nil { + if info, ferr := errFile.Stat(); ferr == nil { + errData := make([]byte, info.Size()) + if _, ferr = errFile.Read(errData); ferr == nil { + stderr = string(errData) + } + } + } + } + if ferr != nil { + stderr = ferr.Error() + } + t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) + t.Fail() + } else { + // t.Logf("stdout: %s", string(outBuf.Bytes())) + f.removeTestDir(workingDir) + } + }) + } + } +} From 3f26aace9d0074458a00f39b9ff11a8f05c327e5 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Fri, 19 Jun 2020 10:30:41 -0400 Subject: [PATCH 063/267] Add Auth Address to V2 accounts endpoint (#1184) Spec update. Generate code. Update handler. --- daemon/algod/api/algod.oas2.json | 5 + daemon/algod/api/algod.oas3.yml | 5 + .../api/server/v2/generated/private/routes.go | 195 +++++++-------- .../api/server/v2/generated/private/types.go | 3 + .../algod/api/server/v2/generated/routes.go | 224 +++++++++--------- daemon/algod/api/server/v2/generated/types.go | 3 + daemon/algod/api/server/v2/handlers.go | 1 + 7 files changed, 228 insertions(+), 208 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index af2e5d3043..4198b89e98 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -930,6 +930,11 @@ "msig", "lsig" ] + }, + "auth-addr": { + "description": "\\[spend\\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field.", + "type": "string", + "x-algorand-format": "Address" } } }, diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index afb2050a5a..7567483b4b 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -604,6 +604,11 @@ }, "type": "array" }, + "auth-addr": { + "description": "\\[spend\\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field.", + "type": "string", + "x-algorand-format": "Address" + }, "created-assets": { "description": "\\[apar\\] parameters of assets created by this account.\n\nNote: the raw account uses `map[int] -> Asset` for this type.", "items": { diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 578938fd62..9adbe275e5 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -236,103 +236,104 @@ func RegisterHandlers(router interface { var swaggerSpec = []string{ "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", - "AAA=", + "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", + "vHt3978BAAD//9RHW+GqYwAA", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index 69db39e0d0..00cfa6da51 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -24,6 +24,9 @@ type Account struct { // Note the raw object uses `map[int] -> AssetHolding` for this type. Assets *[]AssetHolding `json:"assets,omitempty"` + // \[spend\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field. + AuthAddr *string `json:"auth-addr,omitempty"` + // \[apar\] parameters of assets created by this account. // // Note: the raw account uses `map[int] -> Asset` for this type. diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 7a0d295c58..90cda3f6df 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -454,117 +454,119 @@ func RegisterHandlers(router interface { var swaggerSpec = []string{ "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", + "EZxO7qcAAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index ea370ddfda..228233b7f7 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -24,6 +24,9 @@ type Account struct { // Note the raw object uses `map[int] -> AssetHolding` for this type. Assets *[]AssetHolding `json:"assets,omitempty"` + // \[spend\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field. + AuthAddr *string `json:"auth-addr,omitempty"` + // \[apar\] parameters of assets created by this account. // // Note: the raw account uses `map[int] -> Asset` for this type. diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 4567a03c28..ab06adc081 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -180,6 +180,7 @@ func (v2 *Handlers) AccountInformation(ctx echo.Context, address string) error { Participation: apiParticipation, CreatedAssets: &createdAssets, Assets: &assets, + AuthAddr: addrOrNil(record.AuthAddr), } return ctx.JSON(http.StatusOK, response) From eeedd3d6708d6665bb478580f704cf52da1fe9e0 Mon Sep 17 00:00:00 2001 From: egieseke Date: Sat, 20 Jun 2020 13:27:33 -0400 Subject: [PATCH 064/267] Make online flag optional since it defaults to true. (#1170) Since the online option in the goal account changeonlinestatus defaults to true, updated the command so that the online option is not required. --- cmd/goal/account.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 7553a6c4b2..78417e5157 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -125,7 +125,6 @@ func init() { changeOnlineCmd.Flags().StringVarP(&accountAddress, "address", "a", "", "Account address to change (required if no -partkeyfile)") changeOnlineCmd.Flags().StringVarP(&partKeyFile, "partkeyfile", "", "", "Participation key file (required if no -account)") changeOnlineCmd.Flags().BoolVarP(&online, "online", "o", true, "Set this account to online or offline") - changeOnlineCmd.MarkFlagRequired("online") changeOnlineCmd.Flags().Uint64VarP(&transactionFee, "fee", "f", 0, "The Fee to set on the status change transaction (defaults to suggested fee)") changeOnlineCmd.Flags().Uint64VarP(&firstValid, "firstRound", "", 0, "") changeOnlineCmd.Flags().Uint64VarP(&firstValid, "firstvalid", "", 0, "FirstValid for the status change transaction (0 for current)") From a87718f40a3c5a313a3dea87d73df299cc097331 Mon Sep 17 00:00:00 2001 From: egieseke Date: Mon, 22 Jun 2020 11:58:54 -0400 Subject: [PATCH 065/267] Have go-algorand use mule ci jenkins job (#1072) Create a Jenkins mule pipeline to build and test go-algorand. --- Makefile | 26 +- docker/build/Dockerfile | 2 +- docker/build/Dockerfile-deploy | 2 +- ...{arm.Dockerfile => cicd.alpine.Dockerfile} | 2 +- docker/build/cicd.centos.Dockerfile | 10 +- mule.yaml | 281 ++++++++++++------ network/ping_test.go | 2 +- scripts/build_package.sh | 2 +- scripts/build_packages.sh | 3 +- scripts/configure_dev.sh | 39 ++- scripts/release/mule/Makefile.mule | 33 +- scripts/upload_version.sh | 5 +- .../cli/goal/expect/goalExpectCommon.exp | 17 +- .../cli/goal/expect/goalTxValidityTest.exp | 2 +- .../upgrades/send_receive_upgrade_test.go | 2 +- test/muleCI/Jenkinsfile | 3 + test/muleCI/mule.yaml | 199 +++++++++++++ test/scripts/e2e.sh | 64 ++-- test/scripts/e2e_basic_start_stop.sh | 28 +- test/scripts/e2e_client_runner.py | 4 +- test/scripts/e2e_subs/limit-swap-test.sh | 10 +- test/scripts/goal_subcommand_sanity.sh | 5 +- test/scripts/testrunid.py | 6 +- 23 files changed, 562 insertions(+), 185 deletions(-) rename docker/build/{arm.Dockerfile => cicd.alpine.Dockerfile} (94%) create mode 100644 test/muleCI/Jenkinsfile create mode 100644 test/muleCI/mule.yaml diff --git a/Makefile b/Makefile index 68e1afd239..2d6b7da8ff 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,19 @@ export GOPATH := $(shell go env GOPATH) GOPATH1 := $(firstword $(subst :, ,$(GOPATH))) export GO111MODULE := on -export GOPROXY := https://gocenter.io +export GOPROXY := direct UNAME := $(shell uname) SRCPATH := $(shell pwd) ARCH := $(shell ./scripts/archtype.sh) OS_TYPE := $(shell ./scripts/ostype.sh) +S3_RELEASE_BUCKET = $$S3_RELEASE_BUCKET # If build number already set, use it - to ensure same build number across multiple platforms being built BUILDNUMBER ?= $(shell ./scripts/compute_build_number.sh) COMMITHASH := $(shell ./scripts/compute_build_commit.sh) -BUILDBRANCH ?= $(shell ./scripts/compute_branch.sh) -BUILDCHANNEL ?= $(shell ./scripts/compute_branch_channel.sh $(BUILDBRANCH)) +BUILDBRANCH := $(shell ./scripts/compute_branch.sh) +BUILDCHANNEL := $(shell ./scripts/compute_branch_channel.sh $(BUILDBRANCH)) DEFAULTNETWORK ?= $(shell ./scripts/compute_branch_network.sh $(BUILDBRANCH)) DEFAULT_DEADLOCK ?= $(shell ./scripts/compute_branch_deadlock_default.sh $(BUILDBRANCH)) @@ -155,8 +156,14 @@ $(KMD_API_SWAGGER_INJECT): $(KMD_API_SWAGGER_SPEC) $(KMD_API_SWAGGER_SPEC).valid build: buildsrc gen +# We're making an empty file in the go-cache dir to +# get around a bug in go build where it will fail +# to cache binaries from time to time on empty NFS +# dirs buildsrc: crypto/libs/$(OS_TYPE)/$(ARCH)/lib/libsodium.a node_exporter NONGO_BIN deps $(ALGOD_API_SWAGGER_INJECT) $(KMD_API_SWAGGER_INJECT) - go install $(GOTRIMPATH) $(GOTAGS) $(GOBUILDMODE) -ldflags="$(GOLDFLAGS)" ./... + mkdir -p tmp/go-cache && \ + touch tmp/go-cache/file.txt && \ + GOCACHE=$(SRCPATH)/tmp/go-cache go install $(GOTRIMPATH) $(GOTAGS) $(GOBUILDMODE) -ldflags="$(GOLDFLAGS)" ./... SOURCES_RACE := github.com/algorand/go-algorand/cmd/kmd @@ -233,7 +240,10 @@ node_exporter: $(GOPATH1)/bin/node_exporter # The file is was taken from the S3 cloud and it traditionally stored at # /travis-build-artifacts-us-ea-1.algorand.network/algorand/node_exporter/latest/node_exporter-stable-linux-x86_64.tar.gz $(GOPATH1)/bin/node_exporter: - tar -xzvf installer/external/node_exporter-stable-$(shell ./scripts/ostype.sh)-$(shell uname -m | tr '[:upper:]' '[:lower:]').tar.gz -C $(GOPATH1)/bin + mkdir -p $(GOPATH1)/bin && \ + cd $(GOPATH1)/bin && \ + tar -xzvf $(SRCPATH)/installer/external/node_exporter-stable-$(shell ./scripts/ostype.sh)-$(shell uname -m | tr '[:upper:]' '[:lower:]').tar.gz && \ + cd - # deploy @@ -279,3 +289,9 @@ install: build ###### TARGETS FOR CICD PROCESS ###### include ./scripts/release/mule/Makefile.mule +SUPPORTED_ARCHIVE_OS_ARCH = linux/amd64 linux/arm64 linux/arm darwin/amd64 + +archive: + CHANNEL=$(BUILDCHANNEL) \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$${PATH} \ + scripts/upload_version.sh $(BUILDCHANNEL) $(SRCPATH)/tmp/node_pkgs $(S3_RELEASE_BUCKET) diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index 5b04ab3e5d..ac62e8dd55 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /root RUN wget --quiet https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz && tar -xvf go${GOLANG_VERSION}.linux-amd64.tar.gz && mv go /usr/local ENV GOROOT=/usr/local/go \ GOPATH=$HOME/go \ - GOPROXY=https://gocenter.io + GOPROXY=https://gocenter.io,https://goproxy.io,direct RUN mkdir -p $GOPATH/src/github.com/algorand WORKDIR $GOPATH/src/github.com/algorand COPY ./go-algorand ./go-algorand/ diff --git a/docker/build/Dockerfile-deploy b/docker/build/Dockerfile-deploy index 65c16b7f2f..cc534ceb63 100644 --- a/docker/build/Dockerfile-deploy +++ b/docker/build/Dockerfile-deploy @@ -6,7 +6,7 @@ WORKDIR /root RUN wget --quiet https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz && tar -xvf go${GOLANG_VERSION}.linux-amd64.tar.gz && mv go /usr/local ENV GOROOT=/usr/local/go \ GOPATH=$HOME/go \ - GOPROXY=https://gocenter.io + GOPROXY=https://gocenter.io,https://goproxy.io,direct RUN mkdir -p $GOPATH/src/github.com/algorand WORKDIR $GOPATH/src/github.com/algorand COPY . ./go-algorand/ diff --git a/docker/build/arm.Dockerfile b/docker/build/cicd.alpine.Dockerfile similarity index 94% rename from docker/build/arm.Dockerfile rename to docker/build/cicd.alpine.Dockerfile index 8c801cdeed..9824eef64e 100644 --- a/docker/build/arm.Dockerfile +++ b/docker/build/cicd.alpine.Dockerfile @@ -24,7 +24,7 @@ RUN apk add dpkg && \ COPY . $GOPATH/src/github.com/algorand/go-algorand WORKDIR $GOPATH/src/github.com/algorand/go-algorand ENV GCC_CONFIG="--with-arch=armv6" \ - GOPROXY=https://gocenter.io + GOPROXY=https://gocenter.io,https://goproxy.io,direct RUN make ci-deps && make clean RUN rm -rf $GOPATH/src/github.com/algorand/go-algorand && \ mkdir -p $GOPATH/src/github.com/algorand/go-algorand diff --git a/docker/build/cicd.centos.Dockerfile b/docker/build/cicd.centos.Dockerfile index a9a038a42f..3021859e5a 100644 --- a/docker/build/cicd.centos.Dockerfile +++ b/docker/build/cicd.centos.Dockerfile @@ -1,12 +1,15 @@ ARG ARCH="amd64" FROM ${ARCH}/centos:7 -ENV GOLANG_VERSION 1.12 -ARG ARCH="amd64" + RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum update -y && \ - yum install -y autoconf wget awscli git gnupg2 nfs-utils python36 sqlite3 boost-devel expect jq libtool gcc-c++ libstdc++-devel libstdc++-static rpmdevtools createrepo rpm-sign bzip2 which ShellCheck + yum install -y autoconf wget awscli git gnupg2 nfs-utils python3-devel sqlite3 boost-devel expect jq \ + libtool gcc-c++ libstdc++-devel libstdc++-static rpmdevtools createrepo rpm-sign bzip2 which ShellCheck \ + libffi-devel openssl-devel WORKDIR /root +ARG ARCH +ENV GOLANG_VERSION 1.12.17 RUN wget https://dl.google.com/go/go${GOLANG_VERSION}.linux-${ARCH%v*}.tar.gz \ && tar -xvf go${GOLANG_VERSION}.linux-${ARCH%v*}.tar.gz && \ mv go /usr/local @@ -28,4 +31,3 @@ RUN rm -rf $GOPATH/src/github.com/algorand/go-algorand && \ mkdir -p $GOPATH/src/github.com/algorand/go-algorand RUN echo "vm.max_map_count = 262144" >> /etc/sysctl.conf CMD ["/bin/bash"] - diff --git a/mule.yaml b/mule.yaml index 16ba509b41..c30fd5ca51 100644 --- a/mule.yaml +++ b/mule.yaml @@ -1,102 +1,199 @@ -stages: - build-linux-amd64: +tasks: - task: docker.Version - name: linux-amd64 - arch: amd64 configFilePath: scripts/configure_dev-deps.sh - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: amd64 - version: '{{ docker.Version.linux-amd64.version }}' + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' dockerFilePath: docker/build/cicd.Dockerfile + dependencies: docker.Version - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-amd64.version }}' - workDir: /go/src/github.com/algorand/go-algorand - target: fulltest ci-build - test-linux-amd64: - - task: docker.Version - name: linux-amd64 - arch: amd64 - configFilePath: scripts/configure_dev-deps.sh - - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: amd64 - version: '{{ docker.Version.linux-amd64.version }}' - dockerFilePath: docker/build/cicd.Dockerfile - - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-amd64.version }}' - workDir: /go/src/github.com/algorand/go-algorand - target: ci-integration - build-linux-arm64: - - task: docker.Version - name: linux-arm64 - arch: arm64v8 - configFilePath: scripts/configure_dev-deps.sh - - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: arm64v8 - version: '{{ docker.Version.linux-arm64.version }}' - dockerFilePath: docker/build/cicd.Dockerfile + name: build + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: ci-build - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-arm64.version }}' - workDir: /go/src/github.com/algorand/go-algorand - target: fulltest ci-build - test-linux-arm64: - - task: docker.Version - name: linux-arm64 - arch: arm64v8 - configFilePath: scripts/configure_dev-deps.sh - - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: arm64v8 - version: '{{ docker.Version.linux-arm64.version }}' - dockerFilePath: docker/build/cicd.Dockerfile + name: fulltest + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: fulltest -j4 - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-arm64.version }}' - workDir: /go/src/github.com/algorand/go-algorand - target: ci-integration - build-linux-arm: - - task: docker.Version - name: linux-arm - arch: arm32v6 - configFilePath: scripts/configure_dev-deps.sh - - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: arm32v6 - version: '{{ docker.Version.linux-arm.version }}' - dockerFilePath: docker/build/arm.Dockerfile + name: shorttest + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: shorttest -j4 - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-arm.version }}' - workDir: /go/src/github.com/algorand/go-algorand + name: integration-test + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: ci-integration -j4 + - task: shell.Make + name: deps + target: ci-deps + - task: shell.Make + name: build target: ci-build - test-linux-arm: - - task: docker.Version - name: linux-arm - arch: arm32v6 - configFilePath: scripts/configure_dev-deps.sh - - task: shell.docker.Ensure - image: algorand/go-algorand-linux - arch: arm32v6 - version: '{{ docker.Version.linux-arm.version }}' - dockerFilePath: docker/build/arm.Dockerfile - - task: docker.Make - image: algorand/go-algorand-linux - version: '{{ docker.Version.linux-arm.version }}' - workDir: /go/src/github.com/algorand/go-algorand - target: ci-integration - build-local: - task: shell.Make - target: ci-deps fulltest ci-build - test-local: - - task: shell.Make - target: ci-integration - release: - - task: release.notes.GenerateReleaseNotes - releaseVersion: ${GO_ALGORAND_RELEASE_VERSION} - githubPatToken: ${GITHUB_PAT_TOKEN} - githubRepoFullName: algorand/go-algorand + name: fulltest + target: fulltest -j4 + - task: shell.Make + name: shorttest + target: shorttest -j3 + - task: shell.Make + name: integration-test + target: ci-integration -j4 + - task: shell.Make + name: archive + target: archive + +jobs: + # Linux arm64 jobs + build-linux-arm64: + configs: + arch: arm64v8 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.build + test-linux-arm64-fulltest: + configs: + arch: arm64v8 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.fulltest + test-linux-arm64-shorttest: + configs: + arch: arm64v8 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.shorttest + test-linux-arm64-integration: + configs: + arch: arm64v8 + docker: + env: + - SHORTTEST=${SHORTTEST} + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.integration-test + + + + # Linux amd64 jobs + build-linux-amd64: + configs: + arch: amd64 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.build + test-linux-amd64-fulltest: + configs: + arch: amd64 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.fulltest + test-linux-amd64-shorttest: + configs: + arch: amd64 + docker: + env: + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.shorttest + test-linux-amd64-integration: + configs: + arch: amd64 + docker: + env: + - SHORTTEST=${SHORTTEST} + - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} + - TRAVIS_BRANCH=${TRAVIS_BRANCH} + - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} + - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} + tasks: + - shell.docker.Ensure + - docker.Make.integration-test + + + + # Local jobs + build-local: + tasks: + - shell.Make.deps + - shell.Make.build + test-local-fulltest: + tasks: + - shell.Make.deps + - shell.Make.fulltest + test-local-shorttest: + tasks: + - shell.Make.deps + - shell.Make.shorttest + test-local-integration: + tasks: + - shell.Make.deps + - shell.Make.integration-test + archive-local: + tasks: + - shell.Make.deps + - shell.Make.archive + + + + # Linux arm jobs + build-linux-arm: + configs: + arch: arm32v6 + target: ci-build + tasks: + - docker.Make + test-linux-arm-shorttest: + configs: + arch: arm32v6 + target: shorttest + tasks: + - docker.Make diff --git a/network/ping_test.go b/network/ping_test.go index f719798dfe..4914917c83 100644 --- a/network/ping_test.go +++ b/network/ping_test.go @@ -60,7 +60,7 @@ func TestPing(t *testing.T) { if lastPingRoundTripTime > 0 { postPing := time.Now() testTime := postPing.Sub(prePing) - if (lastPingRoundTripTime < testTime) && (lastPingRoundTripTime > (testTime - (2 * waitStep))) { + if (lastPingRoundTripTime < testTime) { // success return } diff --git a/scripts/build_package.sh b/scripts/build_package.sh index 7e99452ee9..47772e1c8b 100755 --- a/scripts/build_package.sh +++ b/scripts/build_package.sh @@ -111,7 +111,7 @@ TOOLS_ROOT=${PKG_ROOT}/tools echo "Staging tools package files" -bin_files=("algons" "auctionconsole" "auctionmaster" "auctionminion" "coroner" "dispenser" "netgoal" "nodecfg" "pingpong" "cc_service" "cc_agent" "cc_client" "COPYING") +bin_files=("algons" "auctionconsole" "auctionmaster" "auctionminion" "coroner" "dispenser" "netgoal" "nodecfg" "pingpong" "cc_service" "cc_agent" "cc_client" "COPYING" "dsign") mkdir -p ${TOOLS_ROOT} for bin in "${bin_files[@]}"; do cp ${GOPATHBIN}/${bin} ${TOOLS_ROOT} diff --git a/scripts/build_packages.sh b/scripts/build_packages.sh index 5a196c4135..bd2592f63e 100755 --- a/scripts/build_packages.sh +++ b/scripts/build_packages.sh @@ -55,8 +55,6 @@ if [ "${PKG_ROOT}" = "" ]; then PKG_ROOT=${HOME}/node_pkg fi -rm -rf ${PKG_ROOT} - BASECHANNEL=${CHANNEL} echo Building ${#VARIATION_ARRAY[@]} variations @@ -85,6 +83,7 @@ for var in "${VARIATION_ARRAY[@]}"; do PKG_NAME=${OS}-${ARCH} PLATFORM_ROOT=${PKG_ROOT}/${CHANNEL}/${PKG_NAME} + rm -rf ${PLATFORM_ROOT} mkdir -p ${PLATFORM_ROOT} scripts/build_package.sh ${OS} ${ARCH} ${PLATFORM_ROOT} diff --git a/scripts/configure_dev.sh b/scripts/configure_dev.sh index 11ce3ac421..4c9df6ea05 100755 --- a/scripts/configure_dev.sh +++ b/scripts/configure_dev.sh @@ -1,15 +1,43 @@ #!/usr/bin/env bash set -e +HELP="Usage: $0 [-s] +Installs host level dependencies necessary to build go-algorand. + +Options: + -s Skips installing go dependencies + -f Force dependencies to be installed (May overwrite existing files) +" + +SKIP_GO_DEPS=false +FORCE=false +while getopts ":sfh" opt; do + case ${opt} in + s ) SKIP_GO_DEPS=true + ;; + f ) FORCE=true + ;; + h ) echo "${HELP}" + exit 0 + ;; + \? ) echo "${HELP}" + exit 2 + ;; + esac +done + SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" OS=$("$SCRIPTPATH"/ostype.sh) function install_or_upgrade { + if ${FORCE} ; then + BREW_FORCE="-f" + fi if brew ls --versions "$1" >/dev/null; then - HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade "$1" || true + HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade ${BREW_FORCE} "$1" || true else - HOMEBREW_NO_AUTO_UPDATE=1 brew install "$1" + HOMEBREW_NO_AUTO_UPDATE=1 brew install ${BREW_FORCE} "$1" fi } @@ -21,7 +49,7 @@ if [ "${OS}" = "linux" ]; then fi sudo apt-get update - sudo apt-get install -y libboost-all-dev expect jq autoconf shellcheck sqlite3 + sudo apt-get install -y libboost-all-dev expect jq autoconf shellcheck sqlite3 python3.7-venv elif [ "${OS}" = "darwin" ]; then brew update brew tap homebrew/cask @@ -32,6 +60,11 @@ elif [ "${OS}" = "darwin" ]; then install_or_upgrade autoconf install_or_upgrade automake install_or_upgrade shellcheck + install_or_upgrade python3 +fi + +if ${SKIP_GO_DEPS} ; then + exit 0 fi "$SCRIPTPATH"/configure_dev-deps.sh diff --git a/scripts/release/mule/Makefile.mule b/scripts/release/mule/Makefile.mule index 6fef5e7ecc..3f8ba746cc 100644 --- a/scripts/release/mule/Makefile.mule +++ b/scripts/release/mule/Makefile.mule @@ -15,7 +15,7 @@ ci-deps: ci-setup: mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) -ci-test: ci-build +ci-test: ifeq ($(ARCH), amd64) RACE=-race else @@ -26,25 +26,18 @@ endif done ci-integration: - -ifeq ($(ARCH), amd64) - export NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/bin && \ - export PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/bin:$$PATH && \ - export PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH && \ - export SRCROOT=$(SRCPATH) && \ - ./test/scripts/e2e_go_tests.sh -else - export NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/bin && \ - export PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/bin:$$PATH && \ - export PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/dev/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH && \ - export SRCROOT=$(SRCPATH) && \ - ./test/scripts/e2e_go_tests.sh -norace -endif - -ci-build: ci-setup buildsrc gen - PKG_ROOT=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) \ - NO_BUILD=True VARIATIONS=$(OS_TYPE)/$(ARCH) \ - scripts/build_packages.sh $(OS_TYPE)/$(ARCH) + NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$$PATH \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/tools:$$PATH \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH \ + SRCROOT=$(SRCPATH) \ + test/scripts/e2e.sh -c $(BUILDCHANNEL) -n + +ci-build: buildsrc gen ci-setup + CHANNEL=$(BUILDCHANNEL) PKG_ROOT=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) NO_BUILD=True VARIATIONS=$(OS_TYPE)-$(ARCH) \ + scripts/build_packages.sh $(OS_TYPE)/$(ARCH) && \ + mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/data && \ + cp gen/devnet/genesis.json $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/data # Builds targets from the sub-directories of ./scripts/release/mule/, such as `mule-package`, `mule-sign`, `mule-test`. # https://scene-si.org/2019/12/04/make-dynamic-makefile-targets/ diff --git a/scripts/upload_version.sh b/scripts/upload_version.sh index 14c17c2fcd..8d01c4f248 100755 --- a/scripts/upload_version.sh +++ b/scripts/upload_version.sh @@ -23,5 +23,6 @@ CHANNEL=$1 DIRECTORY=$2 BUCKET=$3 -export GOPATH=$(go env GOPATH) -${GOPATH}/bin/updater send -s "${DIRECTORY}" -c "${CHANNEL}" -b "${BUCKET}" +export GOPATH1=$(go env GOPATH | cut -d':' -f1 ) +export PATH=${PATH}:${GOPATH1}/bin +updater send -s "${DIRECTORY}" -c "${CHANNEL}" -b "${BUCKET}" diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index ec19b0a033..46b0322a5f 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -141,15 +141,15 @@ proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR set ::GLOBAL_NETWORK_NAME $NETWORK_NAME # Running on ARM64, it seems that network creation is pretty slow. - # 30 second won't be enough here, so I'm changing this to 90 seconds. - set timeout 90 + # 30 second won't be enough here, so I'm changing this to 120 seconds. + set timeout 120 if { [catch { # Create network puts "network create $NETWORK_NAME" spawn goal network create --network $NETWORK_NAME --template $NETWORK_TEMPLATE --datadir $TEST_ALGO_DIR --rootdir $TEST_ROOT_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Timed out creating network" } + timeout { close; ::AlgorandGoal::Abort "Timed out creating network with timeout: $timeout" } "^Network $NETWORK_NAME created under.*" { puts "Network $NETWORK_NAME created" ; close } close } @@ -205,6 +205,7 @@ proc ::AlgorandGoal::StopNetwork { NETWORK_NAME TEST_ALGO_DIR TEST_ROOT_DIR } { # Create a new wallet proc ::AlgorandGoal::CreateWallet { WALLET_NAME WALLET_PASSWORD TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { set WALLET_PASS_PHRASE "NOT SET" spawn goal wallet new $WALLET_NAME -d $TEST_PRIMARY_NODE_DIR @@ -237,6 +238,7 @@ proc ::AlgorandGoal::CreateWallet { WALLET_NAME WALLET_PASSWORD TEST_PRIMARY_NOD # Verify that the wallet exists proc ::AlgorandGoal::VerifyWallet { WALLET_NAME TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { spawn goal wallet list -d $TEST_PRIMARY_NODE_DIR expect { @@ -249,6 +251,7 @@ proc ::AlgorandGoal::VerifyWallet { WALLET_NAME TEST_PRIMARY_NODE_DIR } { } proc ::AlgorandGoal::RecoverWallet { NEW_WALLET_NAME WALLET_PASSPHRASE NEW_WALLET_PASSWORD TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { spawn goal wallet new -r $NEW_WALLET_NAME -d $TEST_PRIMARY_NODE_DIR expect { @@ -273,6 +276,7 @@ proc ::AlgorandGoal::RecoverWallet { NEW_WALLET_NAME WALLET_PASSPHRASE NEW_WALLE # Associate a new account with a specific wallet proc ::AlgorandGoal::CreateAccountForWallet { WALLET_NAME WALLET_PASSWORD TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { spawn goal account new -w $WALLET_NAME -d $TEST_PRIMARY_NODE_DIR while 1 { @@ -291,6 +295,7 @@ proc ::AlgorandGoal::CreateAccountForWallet { WALLET_NAME WALLET_PASSWORD TEST_P # Verify that account exists proc ::AlgorandGoal::VerifyAccount { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { spawn goal account list -w $WALLET_NAME -d $TEST_PRIMARY_NODE_DIR while 1 { @@ -308,12 +313,14 @@ proc ::AlgorandGoal::VerifyAccount { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS # Delete an account proc ::AlgorandGoal::DeleteAccount { WALLET_NAME ACCOUNT_ADDRESS } { + set timeout 60 spawn goal account delete --wallet $WALLET_NAME --address $ACCOUNT_ADDRESS expect {*} } #Select an account from the Wallet proc ::AlgorandGoal::GetAccountAddress { WALLET_NAME TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { spawn goal account list -w $WALLET_NAME -d $TEST_PRIMARY_NODE_DIR expect { @@ -329,6 +336,7 @@ proc ::AlgorandGoal::GetAccountAddress { WALLET_NAME TEST_PRIMARY_NODE_DIR } { # Return the Account Balance proc ::AlgorandGoal::GetAccountBalance { WALLET_NAME ACCOUNT_ADDRESS TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [ catch { spawn goal account balance -w $WALLET_NAME -a $ACCOUNT_ADDRESS -d $TEST_PRIMARY_NODE_DIR expect { @@ -344,6 +352,7 @@ proc ::AlgorandGoal::GetAccountBalance { WALLET_NAME ACCOUNT_ADDRESS TEST_PRIMAR # Return the Account Rewards proc ::AlgorandGoal::GetAccountRewards { WALLET_NAME ACCOUNT_ADDRESS TEST_PRIMARY_NODE_DIR } { + set timeout 60 spawn goal account rewards -w $WALLET_NAME -a $ACCOUNT_ADDRESS -d $TEST_PRIMARY_NODE_DIR expect { timeout { ::AlgorandGoal::Abort "Timed out retrieving account rewards for wallet $WALLET_NAME and account $ACCOUNT_ADDRESS" } @@ -355,6 +364,7 @@ proc ::AlgorandGoal::GetAccountRewards { WALLET_NAME ACCOUNT_ADDRESS TEST_PRIMAR # Account Transfer proc ::AlgorandGoal::AccountTransfer { FROM_WALLET_NAME FROM_WALLET_PASSWORD FROM_ACCOUNT_ADDRESS TRANSFER_AMOUNT TO_ACCOUNT_ADDRESS FEE_AMOUNT TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [ catch { set TRANSACTION_ID "NOT SET" spawn goal clerk send --fee $FEE_AMOUNT --wallet $FROM_WALLET_NAME --amount $TRANSFER_AMOUNT --from $FROM_ACCOUNT_ADDRESS --to $TO_ACCOUNT_ADDRESS -d $TEST_PRIMARY_NODE_DIR -N @@ -371,6 +381,7 @@ proc ::AlgorandGoal::AccountTransfer { FROM_WALLET_NAME FROM_WALLET_PASSWORD FRO # Wait for Account to achieve given balance proc ::AlgorandGoal::WaitForAccountBalance { WALLET_NAME ACCOUNT_ADDRESS EXPECTED_BALANCE TEST_PRIMARY_NODE_DIR } { + set timeout 60 if { [catch { set i 0 while 1 { diff --git a/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp b/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp index f72f1c816f..6fa4d333d2 100644 --- a/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp +++ b/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp @@ -22,7 +22,7 @@ set FILE_COUNTER 1 proc TestLastValidInTx { CMD TX_FILE EXPECTED_LAST_VALID } { set PASSED 0 set LAST_VALID 0 - set timeout 5 + set timeout 60 eval spawn $CMD expect diff --git a/test/e2e-go/upgrades/send_receive_upgrade_test.go b/test/e2e-go/upgrades/send_receive_upgrade_test.go index 4d859b2718..ac65faf88a 100644 --- a/test/e2e-go/upgrades/send_receive_upgrade_test.go +++ b/test/e2e-go/upgrades/send_receive_upgrade_test.go @@ -181,7 +181,7 @@ func testAccountsCanSendMoneyAcrossUpgrade(t *testing.T, templatePath string) { time.Sleep(time.Second) - if time.Now().After(startTime.Add(2 * time.Minute)) { + if time.Now().After(startTime.Add(3 * time.Minute)) { a.Fail("upgrade taking too long") } } diff --git a/test/muleCI/Jenkinsfile b/test/muleCI/Jenkinsfile new file mode 100644 index 0000000000..361781be91 --- /dev/null +++ b/test/muleCI/Jenkinsfile @@ -0,0 +1,3 @@ +@Library('go-algorand-ci') _ + +muleCI('test/muleCI/mule.yaml', '0.0.9') \ No newline at end of file diff --git a/test/muleCI/mule.yaml b/test/muleCI/mule.yaml new file mode 100644 index 0000000000..4c419f1f33 --- /dev/null +++ b/test/muleCI/mule.yaml @@ -0,0 +1,199 @@ +tasks: + # Stash tasks + - task: stash.Stash + name: linux-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-amd64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: darwin-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/darwin-amd64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: linux-arm64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: linux-arm32 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm32 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + + # Unstash tasks + - task: stash.Unstash + name: linux-arm64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm64 + - task: stash.Unstash + name: linux-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-amd64 + - task: stash.Unstash + name: darwin-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/darwin-amd64 + - task: stash.Unstash + name: linux-arm32 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm32 + + # Docker tasks + - task: docker.Version + configFilePath: scripts/configure_dev-deps.sh + - task: shell.docker.Ensure + name: centos + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + dockerFilePath: docker/build/cicd.centos.Dockerfile + dependencies: docker.Version + - task: shell.docker.Ensure + name: alpine + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + dockerFilePath: docker/build/cicd.alpine.Dockerfile + dependencies: docker.Version + - task: docker.Make + name: build + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: ci-build + - task: docker.Make + name: fulltest + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: fulltest -j4 + - task: docker.Make + name: integration-test + docker: + env: + - SHORTTEST=-short + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: ci-integration -j4 + - task: docker.Make + name: archive + docker: + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}' + workDir: /go/src/github.com/algorand/go-algorand + target: ci-archive + + # Local Tasks + - task: shell.Make + name: ci-deps build + target: ci-build + - task: shell.Make + name: fulltest + target: fulltest -j4 + - task: shell.Make + name: integration-test + target: ci-integration -j4 + - task: shell.Make + name: archive + target: archive + +jobs: + # Linux amd64 jobs + build-linux-amd64: + configs: + arch: amd64 + tasks: + - shell.docker.Ensure.centos + - docker.Make.build + - stash.Stash.linux-amd64 + test-linux-amd64-integration: + configs: + arch: amd64 + tasks: + - shell.docker.Ensure.centos + - stash.Unstash.linux-amd64 + - docker.Make.integration-test + test-linux-amd64-fulltest: + configs: + arch: amd64 + tasks: + - shell.docker.Ensure.centos + - docker.Make.fulltest + + # Darwin amd64 jobs +# build-darwin-amd64: +# configs: +# arch: amd64 +# tasks: +# - shell.Make.build +# - stash.Stash.darwin-amd64 +# test-darwin-amd64-integration: +# configs: +# arch: amd64 +# tasks: +# - stash.Unstash.darwin-amd64 +# - shell.Make.integration-test +# test-darwin-amd64-fulltest: +# configs: +# arch: amd64 +# tasks: +# - shell.Make.fulltest + + # Linux arm64 jobs + build-linux-arm64: + configs: + arch: arm64v8 + tasks: + - shell.docker.Ensure.centos + - docker.Make.build + - stash.Stash.linux-arm64 + test-linux-arm64-integration: + configs: + arch: arm64v8 + tasks: + - shell.docker.Ensure.centos + - stash.Unstash.linux-arm64 + - docker.Make.integration-test + + # Linux arm32 jobs + build-linux-arm32: + configs: + arch: arm32v6 + tasks: + - shell.docker.Ensure.alpine + - docker.Make.build + - stash.Stash.linux-arm32 + + # Archive jobs + archive-linux-amd64: + configs: + arch: amd64 + tasks: + - shell.docker.Ensure.centos + - stash.Unstash.linux-amd64 + # - stash.Unstash.darwin-amd64 + - stash.Unstash.linux-arm64 + - stash.Unstash.linux-arm32 + - docker.Make.archive diff --git a/test/scripts/e2e.sh b/test/scripts/e2e.sh index 393e8ae873..50662a93d7 100755 --- a/test/scripts/e2e.sh +++ b/test/scripts/e2e.sh @@ -11,24 +11,33 @@ SCRIPT_PATH="$( cd "$(dirname "$0")" ; pwd -P )" SRCROOT="$(pwd -P)" -# Use same PKG_ROOT location as local_install.sh uses. -PKG_ROOT=$(pwd)/tmp/dev_pkg -rm -rf ${PKG_ROOT} # Purge existing dir if present - export CHANNEL=master -while [ "$1" != "" ]; do - case "$1" in - -c) - shift - export CHANNEL="$1" - ;; - *) - echo "Unknown option" "$1" - exit 1 - ;; - esac - shift +HELP="Usage: $0 [-v] [-u] +Script for running go-algorand e2e tests +Requires: + * pip + * python 3 + * go +Options: + -c Channel of build you are building binaries with this script + -n Run tests without building binaries (Binaries are expected in PATH) +" +NO_BUILD=false +while getopts ":c:nh" opt; do + case ${opt} in + c ) CHANNEL=$OPTARG + ;; + n ) NO_BUILD=true + GO_TEST_ARGS="-norace" + ;; + h ) echo "${HELP}" + exit 0 + ;; + \? ) echo "${HELP}" + exit 2 + ;; + esac done # export TEMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t "tmp") @@ -53,15 +62,19 @@ reset_dirs echo Killing all instances and installing current build pkill -u $(whoami) -x algod || true -./scripts/local_install.sh -c ${CHANNEL} -p ${BINDIR} -d ${DATADIR} +if ! ${NO_BUILD} ; then + ./scripts/local_install.sh -c ${CHANNEL} -p ${BINDIR} -d ${DATADIR} + export PATH=${BINDIR}:${PATH} +fi + +cp gen/devnet/genesis.json ${DATADIR} # check our install -${BINDIR}/algod -v -${BINDIR}/goal -v +algod -v +goal -v -./test/scripts/goal_subcommand_sanity.sh "${BINDIR}" "${TEMPDIR}" +./test/scripts/goal_subcommand_sanity.sh "${TEMPDIR}" -export PATH=${BINDIR}:${PATH} export GOPATH=$(go env GOPATH) # Change current directory to test/scripts so we can just use ./test.sh to exec. @@ -72,7 +85,7 @@ cd "${SCRIPT_PATH}" python3 -m venv ${TEMPDIR}/ve . ${TEMPDIR}/ve/bin/activate ${TEMPDIR}/ve/bin/pip3 install --upgrade pip -${TEMPDIR}/ve/bin/pip3 install py-algorand-sdk cryptography +${TEMPDIR}/ve/bin/pip3 install --upgrade py-algorand-sdk cryptography ${TEMPDIR}/ve/bin/python3 e2e_client_runner.py e2e_subs/*.sh deactivate @@ -82,10 +95,13 @@ export TESTDIR=${TEMPDIR} export TESTDATADIR=${SRCROOT}/test/testdata export SRCROOT=${SRCROOT} -./e2e_go_tests.sh +./e2e_go_tests.sh ${GO_TEST_ARGS} rm -rf ${TEMPDIR} -rm -rf ${PKG_ROOT} + +if ! ${NO_BUILD} ; then + rm -rf ${PKG_ROOT} +fi echo "----------------------------------------------------------------------" echo " DONE: E2E" diff --git a/test/scripts/e2e_basic_start_stop.sh b/test/scripts/e2e_basic_start_stop.sh index 3f519e0378..8d9da35c9a 100755 --- a/test/scripts/e2e_basic_start_stop.sh +++ b/test/scripts/e2e_basic_start_stop.sh @@ -15,12 +15,16 @@ function update_running_count() { } function verify_at_least_one_running() { - update_running_count - if [ ${RUNNING_COUNT} -eq 0 ]; then - echo "algod expected to be running but it isn't" - exit 1 - fi - return 0 + # Starting up can take some time, so wait at least 2 seconds + for TRIES in 1 2 3 4 5; do + update_running_count + if [ ${RUNNING_COUNT} -ge 1 ]; then + return 0 + fi + sleep .4 + done + echo "at least one algod expected to be running but ${RUNNING_COUNT} are running" + exit 1 } function verify_none_running() { @@ -37,7 +41,7 @@ function verify_none_running() { } function verify_one_running() { - # Shutting down can take some time, so retry up to 2 seconds + # Starting up can take some time, so retry up to 2 seconds for TRIES in 1 2 3 4 5; do update_running_count if [ ${RUNNING_COUNT} -eq 1 ]; then @@ -55,17 +59,17 @@ verify_none_running #---------------------- # Test that we can start & stop a generic node with no overrides echo Verifying a generic node will start using goal -${BINDIR}/goal node start -d ${DATADIR} +goal node start -d ${DATADIR} verify_at_least_one_running echo Verifying we can stop it using goal -${BINDIR}/goal node stop -d ${DATADIR} +goal node stop -d ${DATADIR} verify_none_running #---------------------- # Test that we can start a generic node straight with no overrides echo Verifying a generic node will start directly -${BINDIR}/algod -d ${DATADIR} & +algod -d ${DATADIR} & verify_at_least_one_running pkill -u $(whoami) -x algod || true verify_none_running @@ -74,9 +78,9 @@ verify_none_running # Test that we can start a generic node against the datadir # but that we cannot start a second one against same datadir echo Verifying that the datadir algod lock works correctly -${BINDIR}/algod -d ${DATADIR} & +algod -d ${DATADIR} & verify_at_least_one_running -${BINDIR}/algod -d ${DATADIR} & +algod -d ${DATADIR} & verify_at_least_one_running # one should still be running verify_one_running # in fact, exactly one should still be running # clean up diff --git a/test/scripts/e2e_client_runner.py b/test/scripts/e2e_client_runner.py index 5149dffe1c..9660835e39 100755 --- a/test/scripts/e2e_client_runner.py +++ b/test/scripts/e2e_client_runner.py @@ -379,8 +379,8 @@ def main(): sys.exit(1) retcode = 0 - xrun(['goal', 'network', 'create', '-r', netdir, '-n', 'tbd', '-t', os.path.join(gopath, 'src/github.com/algorand/go-algorand/test/testdata/nettemplates/TwoNodes50EachFuture.json')], timeout=30) - xrun(['goal', 'network', 'start', '-r', netdir], timeout=30) + xrun(['goal', 'network', 'create', '-r', netdir, '-n', 'tbd', '-t', os.path.join(gopath, 'src/github.com/algorand/go-algorand/test/testdata/nettemplates/TwoNodes50EachFuture.json')], timeout=90) + xrun(['goal', 'network', 'start', '-r', netdir], timeout=90) atexit.register(goal_network_stop, netdir) env['ALGORAND_DATA'] = os.path.join(netdir, 'Node') diff --git a/test/scripts/e2e_subs/limit-swap-test.sh b/test/scripts/e2e_subs/limit-swap-test.sh index 72084b1eeb..7127a32543 100755 --- a/test/scripts/e2e_subs/limit-swap-test.sh +++ b/test/scripts/e2e_subs/limit-swap-test.sh @@ -33,7 +33,7 @@ ACCOUNT_ALGO_TRADER=$(${gcmd} clerk compile ${TEMPDIR}/limit-order-a.teal -o ${T # setup trader with Algos ${gcmd} clerk send --amount 100000000 --from ${ACCOUNT} --to ${ACCOUNT_ALGO_TRADER} -goal node wait +goal node wait --waittime 30 ${gcmd} clerk send -a 0 -t ${ZERO_ADDRESS} -c ${ACCOUNT} --from-program ${TEMPDIR}/limit-order-a.teal @@ -42,8 +42,9 @@ echo "closeout part b, asset trader" # quick expiration, test closeout ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') -TIMEOUT_ROUND=$((${ROUND} + 6)) -SETUP_ROUND=$((${ROUND} + 5)) + +SETUP_ROUND=$((${ROUND} + 10)) +TIMEOUT_ROUND=$((${SETUP_ROUND} + 1)) sed s/TMPL_ASSET/${ASSET_ID}/g < ${GOPATH}/src/github.com/algorand/go-algorand/tools/teal/templates/limit-order-b.teal.tmpl | sed s/TMPL_SWAPN/137/g | sed s/TMPL_SWAPD/31337/g | sed s/TMPL_TIMEOUT/${TIMEOUT_ROUND}/g | sed s/TMPL_OWN/${ACCOUNT}/g | sed s/TMPL_FEE/100000/g | sed s/TMPL_MINTRD/10000/g > ${TEMPDIR}/limit-order-b.teal @@ -90,8 +91,9 @@ ${gcmd} clerk rawsend -f ${TEMPDIR}/bcloseA.stx echo "test actual swap" ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') -TIMEOUT_ROUND=$((${ROUND} + 200)) + SETUP_ROUND=$((${ROUND} + 199)) +TIMEOUT_ROUND=$((${SETUP_ROUND} + 1)) sed s/TMPL_ASSET/${ASSET_ID}/g < ${GOPATH}/src/github.com/algorand/go-algorand/tools/teal/templates/limit-order-b.teal.tmpl | sed s/TMPL_SWAPN/137/g | sed s/TMPL_SWAPD/31337/g | sed s/TMPL_TIMEOUT/${TIMEOUT_ROUND}/g | sed s/TMPL_OWN/${ACCOUNT}/g | sed s/TMPL_FEE/100000/g | sed s/TMPL_MINTRD/10000/g > ${TEMPDIR}/limit-order-b.teal diff --git a/test/scripts/goal_subcommand_sanity.sh b/test/scripts/goal_subcommand_sanity.sh index 3981e6880e..82fac62e21 100755 --- a/test/scripts/goal_subcommand_sanity.sh +++ b/test/scripts/goal_subcommand_sanity.sh @@ -3,12 +3,11 @@ echo "goal subcommand sanity check" set -e set -x -BINDIR=$1 -TEMPDIR=$2 +TEMPDIR=$1 # Run all `goal ... -h` commands. # This will make sure they work and that there are no conflicting subcommand options. -${BINDIR}/goal helptest > ${TEMPDIR}/helptest +goal helptest > ${TEMPDIR}/helptest if bash -x -e ${TEMPDIR}/helptest > ${TEMPDIR}/helptest.out 2>&1; then # ok echo "goal subcommands ok" diff --git a/test/scripts/testrunid.py b/test/scripts/testrunid.py index d9069a0a16..a1f698f5e9 100755 --- a/test/scripts/testrunid.py +++ b/test/scripts/testrunid.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Computes a name to use to identify a current test run, based on timestamp: # D[DD]HHMMSS @@ -7,8 +7,10 @@ # This mimics the BuildNumber so it can be correlated (ie this is [BuildNumber]MMSS) from datetime import datetime +import time epoch = datetime(2018, 5, 25, 0, 0, 0) d1 = datetime.utcnow() delta = d1 - epoch -print("%d%02d%02d%02d" % (delta.days, d1.hour, d1.minute, d1.second)) + +print(f"{delta.days}{d1.hour}-{int(round(time.time() * 1000))}") From 26c2ac6090a040be745ef3f3bae01ce7d260e921 Mon Sep 17 00:00:00 2001 From: btoll Date: Mon, 22 Jun 2020 12:02:07 -0400 Subject: [PATCH 066/267] Remove all code for generating the releases page (#1187) Code for this has been split out to a separate repository. --- package-deploy.yaml | 22 --- .../deploy/generate_releases_page/deploy.sh | 17 -- .../generate_releases_page.py | 182 ------------------ .../deploy/generate_releases_page/html.tpl | 30 --- .../generate_releases_page/releases_page.css | 35 ---- scripts/release/prod/Jenkinsfile | 6 - .../generate_releases_page | 182 ------------------ .../prod/generate_releases_page/html.tpl | 30 --- .../generate_releases_page/releases_page.css | 35 ---- .../prod/stage/generate_releases_page/run.sh | 8 - 10 files changed, 547 deletions(-) delete mode 100755 scripts/release/mule/deploy/generate_releases_page/deploy.sh delete mode 100755 scripts/release/mule/deploy/generate_releases_page/generate_releases_page.py delete mode 100644 scripts/release/mule/deploy/generate_releases_page/html.tpl delete mode 100644 scripts/release/mule/deploy/generate_releases_page/releases_page.css delete mode 100755 scripts/release/prod/generate_releases_page/generate_releases_page delete mode 100644 scripts/release/prod/generate_releases_page/html.tpl delete mode 100644 scripts/release/prod/generate_releases_page/releases_page.css delete mode 100755 scripts/release/prod/stage/generate_releases_page/run.sh diff --git a/package-deploy.yaml b/package-deploy.yaml index a2c169c7e4..514f078031 100644 --- a/package-deploy.yaml +++ b/package-deploy.yaml @@ -16,18 +16,6 @@ tasks: dockerFilePath: docker/build/mule.go.centos.Dockerfile dependencies: docker.Version - - task: docker.Make - name: generate-releases-page - docker: - image: algorand/mule-linux-ubuntu - version: '{{ docker.Version.outputs.version }}' - workDir: /projects/go-algorand - env: [ - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY - ] - target: mule-deploy-generate_releases_page WORKDIR=/projects/go-algorand - - task: docker.Make name: deb docker: @@ -77,11 +65,6 @@ tasks: src: s3://algorand-devops-misc/tools/gnupg2.2.9_centos7_amd64.tar.bz2 dest: /root - - task: s3.BucketCopy - name: releases-page - src: /tmp/index.html - dest: s3://algorand-releases - - task: s3.BucketCopy name: deploy-dev-deb-repo src: s3://algorand-staging/releases/${CHANNEL}/${VERSION} @@ -99,7 +82,6 @@ jobs: tasks: - shell.docker.Ensure.deb - shell.docker.Ensure.rpm - - docker.Make.generate-releases-page - docker.Make.deb - docker.Make.rpm @@ -119,10 +101,6 @@ jobs: tasks: - s3.BucketCopy.gnupg - package-deploy-releases-page: - tasks: - - s3.BucketCopy.releases-page - package-deploy-rpm-repo: tasks: - s3.BucketCopy.rpm-repo diff --git a/scripts/release/mule/deploy/generate_releases_page/deploy.sh b/scripts/release/mule/deploy/generate_releases_page/deploy.sh deleted file mode 100755 index 5458d62b06..0000000000 --- a/scripts/release/mule/deploy/generate_releases_page/deploy.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -WORKDIR="$5" - -if [ -z "$WORKDIR" ] -then - echo "WORKDIR variable must be defined." - exit 1 -fi - -pushd "$WORKDIR/scripts/release/mule/deploy/generate_releases_page" -./generate_releases_page.py >| /tmp/index.html -popd -mule -f package-deploy.yaml package-deploy-releases-page - diff --git a/scripts/release/mule/deploy/generate_releases_page/generate_releases_page.py b/scripts/release/mule/deploy/generate_releases_page/generate_releases_page.py deleted file mode 100755 index 00e05c4f99..0000000000 --- a/scripts/release/mule/deploy/generate_releases_page/generate_releases_page.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -# -# This script builds https://releases.algorand.com/index.html. -# -# To run: -# ./generate_releases_page.py > index.html - -import sys -import boto3 - -staging_bucket = "algorand-dev-deb-repo" -staging_prefix = "http://algorand-dev-deb-repo.s3-website-us-east-1.amazonaws.com/" -key_url = "https://releases.algorand.com/key.pub" -releases_bucket = "algorand-releases" -releases_prefix = "https://releases.algorand.com/" -html_tpl = "html.tpl" -styles_url = "releases_page.css" - -def get_stage_release_set(response): - prefix = None - all = {} - they = [] - for x in response["Contents"]: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.startswith("tools_") or fname.startswith("install_") or fname.startswith("pending_"): - continue - if prefix is None: - prefix = pre - they.append(x) - elif prefix == pre: - they.append(x) - else: - all[prefix] = they - prefix = None - they = [x] - return all - -def release_set_files(rset): - files = {} - for x in rset: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.startswith("hashes_"): - continue - didsuf = False - for suffix in (".asc", ".sig"): - if fname.endswith(suffix): - froot = fname[:-len(suffix)] - fd = files.get(froot) - if fd is None: - fd = {} - files[froot] = fd - fd[suffix] = x - didsuf = True - break - if didsuf: - continue - fd = files.get(fname) - if fd is None: - fd = {} - files[fname] = fd - fd["file"] = path - fd["Size"] = x["Size"] - return files - -def get_hashes_data(s3, rset): - text = "" - for x in rset: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.endswith(".asc"): - continue - if fname.endswith(".sig"): - continue - if fname.startswith("hashes"): - ob = s3.get_object(Bucket=staging_bucket, Key=path) - text += ob["Body"].read().decode() - return text - -def read_hashes(fin): - by_fname = {} - for line in fin: - if not line: - continue - line = line.strip() - if not line: - continue - if line[0] == "#": - continue - hashstr, fname = line.split() - ob = by_fname.get(fname) - if not ob: - ob = {} - by_fname[fname] = ob - if len(hashstr) == 32: - ob["md5"] = hashstr - elif len(hashstr) == 64: - ob["sha256"] = hashstr - elif len(hashstr) == 128: - ob["sha512"] = hashstr - return by_fname - -def objects_by_fname(they): - out = {} - for x in they: - path = x["Key"] - if path.endswith("/"): - continue - parts = path.rsplit("/", 1) - fname = parts[-1] - out[fname] = x - return out - -def getContent(url): - with open(url, "r") as reader: - content = reader.read() - - return content - -def build_page(channels): - html = getContent(html_tpl).replace("{styles}", getContent(styles_url)) - html = html.replace("{stable}", "".join(channels["stable"])).replace("{beta}", "".join(channels["beta"])) - sys.stdout.write(html) - -def get_furl(release_files, fname, skey): - rfpath = release_files.get(fname) - if rfpath is not None: - return releases_prefix + rfpath["Key"] - else: - return staging_prefix + skey - -def main(): - s3 = boto3.client("s3") - channels = {} - - for channel in ["stable", "beta"]: - staging_response = s3.list_objects_v2(Bucket=staging_bucket, Prefix="releases/" + channel + "/", MaxKeys=100) - release_sets = get_stage_release_set(staging_response) - releases_response = s3.list_objects_v2(Bucket=releases_bucket) - release_files = objects_by_fname(releases_response["Contents"]) - - table = [] - - for key, rset in release_sets.items(): - hashftext = get_hashes_data(s3, rset) - fhashes = read_hashes(hashftext.splitlines()) - files = release_set_files(rset) - - for fname, info in files.items(): - if "file" not in info: - continue - furl = get_furl(release_files, fname, info['file']) - ftext = ''.format(furl, fname) - sig = info.get(".sig") - stext = "" - if sig is not None: - sfname = sig["Key"].rsplit("/", 1)[-1] - surl = get_furl(release_files, sfname, sig["Key"]) - stext = '.sig'.format(surl) - size = info.get("Size", "") - hashes = fhashes.get(fname) - if hashes: - for hn in ("md5", "sha256", "sha512"): - hv = hashes.get(hn) - if hv: - ftext += '
{}
'.format(hn, hv) - if not hashes and not stext: - continue - tbody = ["{}{}{}".format(ftext, size, stext)] - table.append("".join(tbody)) - - # Only add the spacer *after* every set. - table.append('') - - channels[channel] = table - - build_page(channels) - -if __name__ == "__main__": - main() - diff --git a/scripts/release/mule/deploy/generate_releases_page/html.tpl b/scripts/release/mule/deploy/generate_releases_page/html.tpl deleted file mode 100644 index f1c36db6db..0000000000 --- a/scripts/release/mule/deploy/generate_releases_page/html.tpl +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - -

Algorand Releases

-

See Algorand Developer Resources for instructions on installation and getting started

-

The Algorand public key to verify these files (except RPM**) is at https://releases.algorand.com/key.pub

-

The public key for verifying RPMs is https://releases.algorand.com/rpm/rpm_algorand.pub

-

** The RPM package for the 2.0.3 release was signed with the https://releases.algorand.com/key.pub. All other releases will have been signed with the RPM key as noted.

- -
- -

Stable releases

- -{stable} -
FileBytesGPG Signature
- -

Beta releases

- -{beta} -
FileBytesGPG Signature
- - - - diff --git a/scripts/release/mule/deploy/generate_releases_page/releases_page.css b/scripts/release/mule/deploy/generate_releases_page/releases_page.css deleted file mode 100644 index 0e0982ee83..0000000000 --- a/scripts/release/mule/deploy/generate_releases_page/releases_page.css +++ /dev/null @@ -1,35 +0,0 @@ -div.hash { - font-family: monospace; -} - -div.fname { - font-size: 120%; -} - -table { - border-collapse: collapse; -} - -tr.spacer td { - border: 0; - padding: 20px 0; -} - -th { - background-color: #DDD; - border: 2px inset gray; - padding: 10px; -} - -td { - border: 2px inset gray; - padding: 1px; - text-align: center; - vertical-align: middle; - width: 200px; -} - -td:first-child { - text-align: left; -} - diff --git a/scripts/release/prod/Jenkinsfile b/scripts/release/prod/Jenkinsfile index 55a1c5e9f6..90db2c0df2 100644 --- a/scripts/release/prod/Jenkinsfile +++ b/scripts/release/prod/Jenkinsfile @@ -22,12 +22,6 @@ pipeline { sh script: "scripts/release/prod/stage/sync/run.sh ${params.channel} ${params.version}" } } - - stage("generate releases page") { - steps { - sh script: "scripts/release/prod/stage/generate_releases_page/run.sh" - } - } } } diff --git a/scripts/release/prod/generate_releases_page/generate_releases_page b/scripts/release/prod/generate_releases_page/generate_releases_page deleted file mode 100755 index 2e473aa7b5..0000000000 --- a/scripts/release/prod/generate_releases_page/generate_releases_page +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 -# -# This script builds https://releases.algorand.com/index.html. -# -# To run: -# ./update_releases_page > index.html - -import sys -import boto3 - -staging_bucket = "algorand-dev-deb-repo" -staging_prefix = "http://algorand-dev-deb-repo.s3-website-us-east-1.amazonaws.com/" -key_url = "https://releases.algorand.com/key.pub" -releases_bucket = "algorand-releases" -releases_prefix = "https://releases.algorand.com/" -html_tpl = "html.tpl" -styles_url = "releases_page.css" - -def get_stage_release_set(response): - prefix = None - all = {} - they = [] - for x in response["Contents"]: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.startswith("tools_") or fname.startswith("install_") or fname.startswith("pending_"): - continue - if prefix is None: - prefix = pre - they.append(x) - elif prefix == pre: - they.append(x) - else: - all[prefix] = they - prefix = None - they = [x] - return all - -def release_set_files(rset): - files = {} - for x in rset: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.startswith("hashes_"): - continue - didsuf = False - for suffix in (".asc", ".sig"): - if fname.endswith(suffix): - froot = fname[:-len(suffix)] - fd = files.get(froot) - if fd is None: - fd = {} - files[froot] = fd - fd[suffix] = x - didsuf = True - break - if didsuf: - continue - fd = files.get(fname) - if fd is None: - fd = {} - files[fname] = fd - fd["file"] = path - fd["Size"] = x["Size"] - return files - -def get_hashes_data(s3, rset): - text = "" - for x in rset: - path = x["Key"] - pre, fname = path.rsplit("/", 1) - if fname.endswith(".asc"): - continue - if fname.endswith(".sig"): - continue - if fname.startswith("hashes"): - ob = s3.get_object(Bucket=staging_bucket, Key=path) - text += ob["Body"].read().decode() - return text - -def read_hashes(fin): - by_fname = {} - for line in fin: - if not line: - continue - line = line.strip() - if not line: - continue - if line[0] == "#": - continue - hashstr, fname = line.split() - ob = by_fname.get(fname) - if not ob: - ob = {} - by_fname[fname] = ob - if len(hashstr) == 32: - ob["md5"] = hashstr - elif len(hashstr) == 64: - ob["sha256"] = hashstr - elif len(hashstr) == 128: - ob["sha512"] = hashstr - return by_fname - -def objects_by_fname(they): - out = {} - for x in they: - path = x["Key"] - if path.endswith("/"): - continue - parts = path.rsplit("/", 1) - fname = parts[-1] - out[fname] = x - return out - -def getContent(url): - with open(url, "r") as reader: - content = reader.read() - - return content - -def build_page(channels): - html = getContent(html_tpl).replace("{styles}", getContent(styles_url)) - html = html.replace("{stable}", "".join(channels["stable"])).replace("{beta}", "".join(channels["beta"])) - sys.stdout.write(html) - -def get_furl(release_files, fname, skey): - rfpath = release_files.get(fname) - if rfpath is not None: - return releases_prefix + rfpath["Key"] - else: - return staging_prefix + skey - -def main(): - s3 = boto3.client("s3") - channels = {} - - for channel in ["stable", "beta"]: - staging_response = s3.list_objects_v2(Bucket=staging_bucket, Prefix="releases/" + channel + "/", MaxKeys=100) - release_sets = get_stage_release_set(staging_response) - releases_response = s3.list_objects_v2(Bucket=releases_bucket) - release_files = objects_by_fname(releases_response["Contents"]) - - table = [] - - for key, rset in release_sets.items(): - hashftext = get_hashes_data(s3, rset) - fhashes = read_hashes(hashftext.splitlines()) - files = release_set_files(rset) - - for fname, info in files.items(): - if "file" not in info: - continue - furl = get_furl(release_files, fname, info['file']) - ftext = ''.format(furl, fname) - sig = info.get(".sig") - stext = "" - if sig is not None: - sfname = sig["Key"].rsplit("/", 1)[-1] - surl = get_furl(release_files, sfname, sig["Key"]) - stext = '.sig'.format(surl) - size = info.get("Size", "") - hashes = fhashes.get(fname) - if hashes: - for hn in ("md5", "sha256", "sha512"): - hv = hashes.get(hn) - if hv: - ftext += '
{}
'.format(hn, hv) - if not hashes and not stext: - continue - tbody = ["{}{}{}".format(ftext, size, stext)] - table.append("".join(tbody)) - - # Only add the spacer *after* every set. - table.append('') - - channels[channel] = table - - build_page(channels) - -if __name__ == "__main__": - main() - diff --git a/scripts/release/prod/generate_releases_page/html.tpl b/scripts/release/prod/generate_releases_page/html.tpl deleted file mode 100644 index f1c36db6db..0000000000 --- a/scripts/release/prod/generate_releases_page/html.tpl +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - -

Algorand Releases

-

See Algorand Developer Resources for instructions on installation and getting started

-

The Algorand public key to verify these files (except RPM**) is at https://releases.algorand.com/key.pub

-

The public key for verifying RPMs is https://releases.algorand.com/rpm/rpm_algorand.pub

-

** The RPM package for the 2.0.3 release was signed with the https://releases.algorand.com/key.pub. All other releases will have been signed with the RPM key as noted.

- -
- -

Stable releases

- -{stable} -
FileBytesGPG Signature
- -

Beta releases

- -{beta} -
FileBytesGPG Signature
- - - - diff --git a/scripts/release/prod/generate_releases_page/releases_page.css b/scripts/release/prod/generate_releases_page/releases_page.css deleted file mode 100644 index 0e0982ee83..0000000000 --- a/scripts/release/prod/generate_releases_page/releases_page.css +++ /dev/null @@ -1,35 +0,0 @@ -div.hash { - font-family: monospace; -} - -div.fname { - font-size: 120%; -} - -table { - border-collapse: collapse; -} - -tr.spacer td { - border: 0; - padding: 20px 0; -} - -th { - background-color: #DDD; - border: 2px inset gray; - padding: 10px; -} - -td { - border: 2px inset gray; - padding: 1px; - text-align: center; - vertical-align: middle; - width: 200px; -} - -td:first-child { - text-align: left; -} - diff --git a/scripts/release/prod/stage/generate_releases_page/run.sh b/scripts/release/prod/stage/generate_releases_page/run.sh deleted file mode 100755 index 6767a5be1d..0000000000 --- a/scripts/release/prod/stage/generate_releases_page/run.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -cd scripts/release/prod/generate_releases_page -./generate_releases_page >| index.html -aws s3 cp index.html s3://algorand-releases - From afbf619a87f930a9ff6d56a73880d5549bc2a696 Mon Sep 17 00:00:00 2001 From: btoll Date: Mon, 22 Jun 2020 12:06:27 -0400 Subject: [PATCH 067/267] Only add the latest version to the rpm repo (#1181) This won't overwrite the current repo on s3. It will append to it. Doing it this way ensures that the latest release is downloaded and installed via yum. --- scripts/release/mule/deploy/rpm/deploy.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/release/mule/deploy/rpm/deploy.sh b/scripts/release/mule/deploy/rpm/deploy.sh index 8f7ad37b88..35a0abb281 100755 --- a/scripts/release/mule/deploy/rpm/deploy.sh +++ b/scripts/release/mule/deploy/rpm/deploy.sh @@ -3,9 +3,11 @@ set -ex +VERSION=$(./scripts/compute_build_number.sh -f) + mule -f package-deploy.yaml package-deploy-setup-gnupg -cd /root +pushd /root tar jxf gnupg*.tar.bz2 export PATH=/root/gnupg2/bin:"${PATH}" @@ -22,10 +24,6 @@ else echo "no-autostart" >> .gnupg/gpg.conf fi -#gpg --import /root/keys/dev.pub -#gpg --import /root/keys/rpm.pub -#rpmkeys --import /root/keys/rpm.pub - echo "wat" | gpg -u rpm@algorand.com --clearsign cat << EOF > .rpmmacros @@ -41,7 +39,7 @@ rpm.addSign(sys.argv[1], '') EOF mkdir rpmrepo -for rpm in $(ls packages/rpm/stable/*.rpm) +for rpm in $(ls packages/rpm/stable/*"$VERSION"*.rpm) do python2 rpmsign.py "$rpm" cp -p "$rpm" rpmrepo @@ -51,5 +49,7 @@ createrepo --database rpmrepo rm -f rpmrepo/repodata/repomd.xml.asc gpg -u rpm@algorand.com --detach-sign --armor rpmrepo/repodata/repomd.xml +popd + mule -f package-deploy.yaml package-deploy-rpm-repo From 0baf48115a596223dc38cb6fb87e5cca6d922592 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 22 Jun 2020 12:57:44 -0400 Subject: [PATCH 068/267] Restructure config package to use reflection (#1139) Existing config files are using hard-coded data structure to represent versioned configuration setting. The per-version structures need to be duplicated with every change to the configuration structure, which makes it error prone, especially when dealing with non-default field values. ( since these non-default field values require some explicit written code to handle the config version upgrades ) Replacing that with a configuration struct which is self-tagged with the default values, allow us to auto-generate the above logic and avoid programmatic mistakes. --- config/config.go | 159 ++--- config/config_test.go | 173 ++--- config/defaultsGenerator/defaultsGenerator.go | 182 ++++++ config/local_defaults.go | 617 +----------------- config/migrate.go | 200 ++++++ installer/config.json.example | 26 +- 6 files changed, 601 insertions(+), 756 deletions(-) create mode 100644 config/defaultsGenerator/defaultsGenerator.go create mode 100644 config/migrate.go diff --git a/config/config.go b/config/config.go index a84f53dc90..eb83f13b33 100644 --- a/config/config.go +++ b/config/config.go @@ -46,42 +46,55 @@ const Mainnet protocol.NetworkID = "mainnet" const GenesisJSONFile = "genesis.json" // Local holds the per-node-instance configuration settings for the protocol. +// !!! WARNING !!! +// +// These versioned struct tags need to be maintained CAREFULLY and treated +// like UNIVERSAL CONSTANTS - they should not be modified once committed. +// +// New fields may be added to the Local struct, along with a version tag +// denoting a new version. When doing so, also update the +// test/testdata/configs/config-v{n}.json and call "make generate" to regenerate the constants. +// +// !!! WARNING !!! type Local struct { // Version tracks the current version of the defaults so we can migrate old -> new // This is specifically important whenever we decide to change the default value - // for an existing parameter. - Version uint32 + // for an existing parameter. This field tag must be updated any time we add a new version. + Version uint32 `version[0]:"0" version[1]:"1" version[2]:"2" version[3]:"3" version[4]:"4" version[5]:"5" version[6]:"6" version[7]:"7" version[8]:"8" version[9]:"9"` // environmental (may be overridden) // When enabled, stores blocks indefinitally, otherwise, only the most recents blocks // are being kept around. ( the precise number of recent blocks depends on the consensus parameters ) - Archival bool + Archival bool `version[0]:"false"` // gossipNode.go // how many peers to propagate to? - GossipFanout int - NetAddress string - ReconnectTime time.Duration + GossipFanout int `version[0]:"4"` + NetAddress string `version[0]:""` + + // 1 * time.Minute = 60000000000 ns + ReconnectTime time.Duration `version[0]:"60" version[1]:"60000000000"` + // what we should tell people to connect to - PublicAddress string + PublicAddress string `version[0]:""` - MaxConnectionsPerIP int + MaxConnectionsPerIP int `version[3]:"30"` // 0 == disable - PeerPingPeriodSeconds int + PeerPingPeriodSeconds int `version[0]:"0"` // for https serving - TLSCertFile string - TLSKeyFile string + TLSCertFile string `version[0]:""` + TLSKeyFile string `version[0]:""` // Logging - BaseLoggerDebugLevel uint32 + BaseLoggerDebugLevel uint32 `version[0]:"1" version[1]:"4"` // if this is 0, do not produce agreement.cadaver - CadaverSizeTarget uint64 + CadaverSizeTarget uint64 `version[0]:"1073741824"` // IncomingConnectionsLimit specifies the max number of long-lived incoming // connections. 0 means no connections allowed. -1 is unbounded. - IncomingConnectionsLimit int + IncomingConnectionsLimit int `version[0]:"-1" version[1]:"10000"` // BroadcastConnectionsLimit specifies the number of connections that // will receive broadcast (gossip) messages from this node. If the @@ -90,38 +103,38 @@ type Local struct { // by money held by peers based on their participation key). 0 means // no outgoing messages (not even transaction broadcasting to outgoing // peers). -1 means unbounded (default). - BroadcastConnectionsLimit int + BroadcastConnectionsLimit int `version[4]:"-1"` // AnnounceParticipationKey specifies that this node should announce its // participation key (with the largest stake) to its gossip peers. This // allows peers to prioritize our connection, if necessary, in case of a // DoS attack. Disabling this means that the peers will not have any // additional information to allow them to prioritize our connection. - AnnounceParticipationKey bool + AnnounceParticipationKey bool `version[4]:"true"` // PriorityPeers specifies peer IP addresses that should always get // outgoing broadcast messages from this node. - PriorityPeers map[string]bool + PriorityPeers map[string]bool `version[4]:""` // To make sure the algod process does not run out of FDs, algod ensures // that RLIMIT_NOFILE exceeds the max number of incoming connections (i.e., // IncomingConnectionsLimit) by at least ReservedFDs. ReservedFDs are meant // to leave room for short-lived FDs like DNS queries, SQLite files, etc. - ReservedFDs uint64 + ReservedFDs uint64 `version[2]:"256"` // local server // API endpoint address - EndpointAddress string + EndpointAddress string `version[0]:"127.0.0.1:0"` // timeouts passed to the rest http.Server implementation - RestReadTimeoutSeconds int - RestWriteTimeoutSeconds int + RestReadTimeoutSeconds int `version[4]:"15"` + RestWriteTimeoutSeconds int `version[4]:"120"` // SRV-based phonebook - DNSBootstrapID string + DNSBootstrapID string `version[0]:".algorand.network"` // Log file size limit in bytes - LogSizeLimit uint64 + LogSizeLimit uint64 `version[0]:"1073741824"` // text/template for creating log archive filename. // Available template vars: @@ -131,129 +144,129 @@ type Local struct { // If the filename ends with .gz or .bz2 it will be compressed. // // default: "node.archive.log" (no rotation, clobbers previous archive) - LogArchiveName string + LogArchiveName string `version[4]:"node.archive.log"` // LogArchiveMaxAge will be parsed by time.ParseDuration(). // Valid units are 's' seconds, 'm' minutes, 'h' hours - LogArchiveMaxAge string + LogArchiveMaxAge string `version[4]:""` // number of consecutive attempts to catchup after which we replace the peers we're connected to - CatchupFailurePeerRefreshRate int + CatchupFailurePeerRefreshRate int `version[0]:"10"` // where should the node exporter listen for metrics - NodeExporterListenAddress string + NodeExporterListenAddress string `version[0]:":9100"` // enable metric reporting flag - EnableMetricReporting bool + EnableMetricReporting bool `version[0]:"false"` // enable top accounts reporting flag - EnableTopAccountsReporting bool + EnableTopAccountsReporting bool `version[0]:"false"` // enable agreement reporting flag. Currently only prints additional period events. - EnableAgreementReporting bool + EnableAgreementReporting bool `version[3]:"false"` // enable agreement timing metrics flag - EnableAgreementTimeMetrics bool + EnableAgreementTimeMetrics bool `version[3]:"false"` // The path to the node exporter. - NodeExporterPath string + NodeExporterPath string `version[0]:"./node_exporter"` // The fallback DNS resolver address that would be used if the system resolver would fail to retrieve SRV records - FallbackDNSResolverAddress string + FallbackDNSResolverAddress string `version[0]:""` // exponential increase factor of transaction pool's fee threshold, should always be 2 in production - TxPoolExponentialIncreaseFactor uint64 + TxPoolExponentialIncreaseFactor uint64 `version[0]:"2"` - SuggestedFeeBlockHistory int + SuggestedFeeBlockHistory int `version[0]:"3"` // TxPoolSize is the number of transactions that fit in the transaction pool - TxPoolSize int + TxPoolSize int `version[0]:"50000" version[5]:"15000"` // number of seconds allowed for syncing transactions - TxSyncTimeoutSeconds int64 + TxSyncTimeoutSeconds int64 `version[0]:"30"` // number of seconds between transaction synchronizations - TxSyncIntervalSeconds int64 + TxSyncIntervalSeconds int64 `version[0]:"60"` // the number of incoming message hashes buckets. - IncomingMessageFilterBucketCount int + IncomingMessageFilterBucketCount int `version[0]:"5"` // the size of each incoming message hash bucket. - IncomingMessageFilterBucketSize int + IncomingMessageFilterBucketSize int `version[0]:"512"` // the number of outgoing message hashes buckets. - OutgoingMessageFilterBucketCount int + OutgoingMessageFilterBucketCount int `version[0]:"3"` // the size of each outgoing message hash bucket. - OutgoingMessageFilterBucketSize int + OutgoingMessageFilterBucketSize int `version[0]:"128"` // enable the filtering of outgoing messages - EnableOutgoingNetworkMessageFiltering bool + EnableOutgoingNetworkMessageFiltering bool `version[0]:"true"` // enable the filtering of incoming messages - EnableIncomingMessageFilter bool + EnableIncomingMessageFilter bool `version[0]:"false"` // control enabling / disabling deadlock detection. // negative (-1) to disable, positive (1) to enable, 0 for default. - DeadlockDetection int + DeadlockDetection int `version[1]:"0"` // Prefer to run algod Hosted (under algoh) // Observed by `goal` for now. - RunHosted bool + RunHosted bool `version[3]:"false"` // The maximal number of blocks that catchup will fetch in parallel. // If less than Protocol.SeedLookback, then Protocol.SeedLookback will be used as to limit the catchup. - CatchupParallelBlocks uint64 + CatchupParallelBlocks uint64 `version[3]:"50" version[5]:"16"` // Generate AssembleBlockMetrics telemetry event - EnableAssembleStats bool + EnableAssembleStats bool `version[0]:""` // Generate ProcessBlockMetrics telemetry event - EnableProcessBlockStats bool + EnableProcessBlockStats bool `version[0]:""` // SuggestedFeeSlidingWindowSize is number of past blocks that will be considered in computing the suggested fee - SuggestedFeeSlidingWindowSize uint32 + SuggestedFeeSlidingWindowSize uint32 `version[3]:"50"` // the max size the sync server would return - TxSyncServeResponseSize int + TxSyncServeResponseSize int `version[3]:"1000000"` // IsIndexerActive indicates whether to activate the indexer for fast retrieval of transactions // Note -- Indexer cannot operate on non Archival nodes - IsIndexerActive bool + IsIndexerActive bool `version[3]:"false"` // UseXForwardedForAddress indicates whether or not the node should use the X-Forwarded-For HTTP Header when // determining the source of a connection. If used, it should be set to the string "X-Forwarded-For", unless the // proxy vendor provides another header field. In the case of CloudFlare proxy, the "CF-Connecting-IP" header // field can be used. - UseXForwardedForAddressField string + UseXForwardedForAddressField string `version[0]:""` // ForceRelayMessages indicates whether the network library relay messages even in the case that no NetAddress was specified. - ForceRelayMessages bool + ForceRelayMessages bool `version[0]:"false"` // ConnectionsRateLimitingWindowSeconds is being used in conjunction with ConnectionsRateLimitingCount; // see ConnectionsRateLimitingCount description for further information. Providing a zero value // in this variable disables the connection rate limiting. - ConnectionsRateLimitingWindowSeconds uint + ConnectionsRateLimitingWindowSeconds uint `version[4]:"1"` // ConnectionsRateLimitingCount is being used along with ConnectionsRateLimitingWindowSeconds to determine if // a connection request should be accepted or not. The gossip network examine all the incoming requests in the past // ConnectionsRateLimitingWindowSeconds seconds that share the same origin. If the total count exceed the ConnectionsRateLimitingCount // value, the connection is refused. - ConnectionsRateLimitingCount uint + ConnectionsRateLimitingCount uint `version[4]:"60"` // EnableRequestLogger enabled the logging of the incoming requests to the telemetry server. - EnableRequestLogger bool + EnableRequestLogger bool `version[4]:"false"` // PeerConnectionsUpdateInterval defines the interval at which the peer connections information is being sent to the // telemetry ( when enabled ). Defined in seconds. - PeerConnectionsUpdateInterval int + PeerConnectionsUpdateInterval int `version[5]:"3600"` // EnableProfiler enables the go pprof endpoints, should be false if // the algod api will be exposed to untrusted individuals - EnableProfiler bool + EnableProfiler bool `version[0]:"false"` // TelemetryToLog records messages to node.log that are normally sent to remote event monitoring - TelemetryToLog bool + TelemetryToLog bool `version[5]:"true"` // DNSSecurityFlags instructs algod validating DNS responses. // Possible fla values @@ -262,48 +275,48 @@ type Local struct { // 0x02 (dnssecRelayAddr) - validate relays' names to addresses resolution // 0x04 (dnssecTelemetryAddr) - validate telemetry and metrics names to addresses resolution // ... - DNSSecurityFlags uint32 + DNSSecurityFlags uint32 `version[6]:"1"` // EnablePingHandler controls whether the gossip node would respond to ping messages with a pong message. - EnablePingHandler bool + EnablePingHandler bool `version[6]:"true"` // DisableOutgoingConnectionThrottling disables the connection throttling of the network library, which // allow the network library to continuesly disconnect relays based on their relative ( and absolute ) performance. - DisableOutgoingConnectionThrottling bool + DisableOutgoingConnectionThrottling bool `version[5]:"false"` // NetworkProtocolVersion overrides network protocol version ( if present ) - NetworkProtocolVersion string + NetworkProtocolVersion string `version[6]:""` // CatchpointInterval set the interval at which catchpoint are being generated. - CatchpointInterval uint64 + CatchpointInterval uint64 `version[7]:"10000"` // CatchpointFileHistoryLength defines how many catchpoint files we want to store back. // 0 means don't store any, -1 mean unlimited and positive number suggest the number of most recent catchpoint files. - CatchpointFileHistoryLength int + CatchpointFileHistoryLength int `version[7]:"365"` // EnableLedgerService enables the ledger serving service. The functionality of this depends on NetAddress, which must also be provided. // This functionality is required for the catchpoint catchup. - EnableLedgerService bool + EnableLedgerService bool `version[7]:"false"` // EnableBlockService enables the block serving service. The functionality of this depends on NetAddress, which must also be provided. // This functionality is required for the catchup. - EnableBlockService bool + EnableBlockService bool `version[7]:"false"` // EnableGossipBlockService enables the block serving service over the gossip network. The functionality of this depends on NetAddress, which must also be provided. // This functionality is required for the relays to perform catchup from nodes. - EnableGossipBlockService bool + EnableGossipBlockService bool `version[8]:"true"` // CatchupHTTPBlockFetchTimeoutSec controls how long the http query for fetching a block from a relay would take before giving up and trying another relay. - CatchupHTTPBlockFetchTimeoutSec int + CatchupHTTPBlockFetchTimeoutSec int `version[9]:"4"` // CatchupGossipBlockFetchTimeoutSec controls how long the gossip query for fetching a block from a relay would take before giving up and trying another relay. - CatchupGossipBlockFetchTimeoutSec int + CatchupGossipBlockFetchTimeoutSec int `version[9]:"4"` // CatchupLedgerDownloadRetryAttempts controls the number of attempt the ledger fetching would be attempted before giving up catching up to the provided catchpoint. - CatchupLedgerDownloadRetryAttempts int + CatchupLedgerDownloadRetryAttempts int `version[9]:"50"` // CatchupLedgerDownloadRetryAttempts controls the number of attempt the block fetching would be attempted before giving up catching up to the provided catchpoint. - CatchupBlockDownloadRetryAttempts int + CatchupBlockDownloadRetryAttempts int `version[9]:"1000"` } // Filenames of config files within the configdir (e.g. ~/.algorand) diff --git a/config/config_test.go b/config/config_test.go index 213db2cba7..9696698f4b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -19,6 +19,7 @@ package config import ( "bytes" "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -246,7 +247,7 @@ func TestConfigMigrate(t *testing.T) { t.Skip() a := require.New(t) - c0, err := loadWithoutDefaults(defaultLocalV0) + c0, err := loadWithoutDefaults(getVersionedDefaultLocalConfig(0)) a.NoError(err) c0, err = migrate(c0) a.NoError(err) @@ -256,13 +257,13 @@ func TestConfigMigrate(t *testing.T) { a.Equal(defaultLocal, c0) a.Equal(defaultLocal, cLatest) - cLatest.Version = configVersion + 1 + cLatest.Version = getLatestConfigVersion() + 1 _, err = migrate(cLatest) a.Error(err) // Ensure we don't migrate values that aren't the default old version - c0Modified := defaultLocalV0 - c0Modified.BaseLoggerDebugLevel = defaultLocalV0.BaseLoggerDebugLevel + 1 + c0Modified := getVersionedDefaultLocalConfig(0) + c0Modified.BaseLoggerDebugLevel = getVersionedDefaultLocalConfig(0).BaseLoggerDebugLevel + 1 c0Modified, err = migrate(c0Modified) a.NoError(err) a.NotEqual(defaultLocal, c0Modified) @@ -275,37 +276,15 @@ func TestConfigMigrateFromDisk(t *testing.T) { a.NoError(err) configsPath := filepath.Join(ourPath, "../test/testdata/configs") - c0, err := loadConfigFromFile(filepath.Join(configsPath, "config-v0.json")) - a.NoError(err) - modified, err := migrate(c0) - a.NoError(err) - a.Equal(defaultLocal, modified) - - c1, err := loadConfigFromFile(filepath.Join(configsPath, "config-v1.json")) - a.NoError(err) - modified, err = migrate(c1) - a.NoError(err) - a.Equal(defaultLocal, modified) - - c2, err := loadConfigFromFile(filepath.Join(configsPath, "config-v2.json")) - a.NoError(err) - modified, err = migrate(c2) - a.NoError(err) - a.Equal(defaultLocal, modified) - - c3, err := loadConfigFromFile(filepath.Join(configsPath, "config-v3.json")) - a.NoError(err) - modified, err = migrate(c3) - a.NoError(err) - a.Equal(defaultLocal, modified) - - c4, err := loadConfigFromFile(filepath.Join(configsPath, "config-v4.json")) - a.NoError(err) - modified, err = migrate(c4) - a.NoError(err) - a.Equal(defaultLocal, modified) + for configVersion := uint32(0); configVersion <= getLatestConfigVersion(); configVersion++ { + c, err := loadConfigFromFile(filepath.Join(configsPath, fmt.Sprintf("config-v%d.json", configVersion))) + a.NoError(err) + modified, err := migrate(c) + a.NoError(err) + a.Equal(defaultLocal, modified) + } - cNext := Local{Version: configVersion + 1} + cNext := Local{Version: getLatestConfigVersion() + 1} _, err = migrate(cNext) a.Error(err) } @@ -314,68 +293,23 @@ func TestConfigMigrateFromDisk(t *testing.T) { func TestConfigInvariant(t *testing.T) { a := require.New(t) - a.Equal(uint32(9), configVersion, "If you bump Config Version, please update this test (and consider if you should be adding more)") - ourPath, err := os.Getwd() a.NoError(err) configsPath := filepath.Join(ourPath, "../test/testdata/configs") - c0 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v0.json"), &c0) - a.NoError(err) - a.Equal(defaultLocalV0, c0) - - c1 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v1.json"), &c1) - a.NoError(err) - a.Equal(defaultLocalV1, c1) - - c2 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v2.json"), &c2) - a.NoError(err) - a.Equal(defaultLocalV2, c2) - - c3 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v3.json"), &c3) - a.NoError(err) - a.Equal(defaultLocalV3, c3) - - c4 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v4.json"), &c4) - a.NoError(err) - a.Equal(defaultLocalV4, c4) - - c5 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v5.json"), &c5) - a.NoError(err) - a.Equal(defaultLocalV5, c5) - - c6 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v6.json"), &c6) - a.NoError(err) - a.Equal(defaultLocalV6, c6) - - c7 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v7.json"), &c7) - a.NoError(err) - a.Equal(defaultLocalV7, c7) - - c8 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v8.json"), &c8) - a.NoError(err) - a.Equal(defaultLocalV8, c8) - - c9 := Local{} - err = codecs.LoadObjectFromFile(filepath.Join(configsPath, "config-v9.json"), &c9) - a.NoError(err) - a.Equal(defaultLocalV9, c9) + for configVersion := uint32(0); configVersion <= getLatestConfigVersion(); configVersion++ { + c := Local{} + err = codecs.LoadObjectFromFile(filepath.Join(configsPath, fmt.Sprintf("config-v%d.json", configVersion)), &c) + a.NoError(err) + a.Equal(getVersionedDefaultLocalConfig(configVersion), c) + } } func TestConfigLatestVersion(t *testing.T) { a := require.New(t) // Make sure current version is correct for the assigned defaultLocal - a.Equal(configVersion, defaultLocal.Version) + a.Equal(getLatestConfigVersion(), defaultLocal.Version) } func TestConsensusUpgrades(t *testing.T) { @@ -493,3 +427,70 @@ func TestLocal_DNSBootstrap(t *testing.T) { }) } } + +func TestLocalStructTags(t *testing.T) { + localType := reflect.TypeOf(Local{}) + + versionField, ok := localType.FieldByName("Version") + require.True(t, true, ok) + ver := 0 + versionTags := []string{} + for { + _, has := versionField.Tag.Lookup(fmt.Sprintf("version[%d]", ver)) + if !has { + break + } + versionTags = append(versionTags, fmt.Sprintf("version[%d]", ver)) + ver++ + } + + for fieldNum := 0; fieldNum < localType.NumField(); fieldNum++ { + field := localType.Field(fieldNum) + if field.Tag == "" { + require.Failf(t, "Field is missing versioning information", "Field Name: %s", field.Name) + } + // the field named "Version" is tested separatly in TestLocalVersionField, so we'll be skipping + // it on this test. + if field.Name == "Version" { + continue + } + // check to see if we have at least a single version from the versions tags above. + foundTag := false + expectedTag := "" + for _, ver := range versionTags { + if val, found := field.Tag.Lookup(ver); found { + foundTag = true + expectedTag = expectedTag + ver + ":\"" + val + "\" " + } + } + require.True(t, foundTag) + expectedTag = expectedTag[:len(expectedTag)-1] + require.Equal(t, expectedTag, string(field.Tag)) + } +} + +func TestGetVersionedDefaultLocalConfig(t *testing.T) { + for i := uint32(0); i < getLatestConfigVersion(); i++ { + localVersion := getVersionedDefaultLocalConfig(i) + require.Equal(t, uint32(i), localVersion.Version) + } +} + +// TestLocalVersionField - ensures the Version contains only versions tags, the versions are all contiguous, and that no non-version tags are included there. +func TestLocalVersionField(t *testing.T) { + localType := reflect.TypeOf(Local{}) + field, ok := localType.FieldByName("Version") + require.True(t, true, ok) + ver := 0 + expectedTag := "" + for { + val, has := field.Tag.Lookup(fmt.Sprintf("version[%d]", ver)) + if !has { + break + } + expectedTag = fmt.Sprintf("%sversion[%d]:\"%s\" ", expectedTag, ver, val) + ver++ + } + expectedTag = expectedTag[:len(expectedTag)-1] + require.Equal(t, expectedTag, string(field.Tag)) +} diff --git a/config/defaultsGenerator/defaultsGenerator.go b/config/defaultsGenerator/defaultsGenerator.go new file mode 100644 index 0000000000..fa7200285b --- /dev/null +++ b/config/defaultsGenerator/defaultsGenerator.go @@ -0,0 +1,182 @@ +// 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 . + +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "reflect" + "sort" + "strings" + "time" + + "github.com/algorand/go-algorand/config" +) + +const defaultLocalVariableDeclaration = "var defaultLocal" + +var outputfilename = flag.String("o", "", "Name of the file where the generated datastructure would go to.") +var packageName = flag.String("p", "", "Name of the package.") +var headerFileName = flag.String("h", "", "Name of the header filename") +var jsonExampleFileName = flag.String("j", "", "Name of the json example file") + +var autoGenHeader = ` +// This file was auto generated by ./config/defaultsGenerator/defaultsGenerator.go, and SHOULD NOT BE MODIFIED in any way +// If you want to make changes to this file, make the corresponding changes to Local in config.go and run "go generate". +` + +// printExit prints the given formatted string ( i.e. just like fmt.Printf ), with the defaultGenerator executable program name +// at the begining, and exit the process with a error code of 1. +func printExit(fmtStr string, args ...interface{}) { + fmt.Printf("%s: "+fmtStr, append([]interface{}{filepath.Base(os.Args[0])}, args...)...) + os.Exit(1) +} + +func main() { + + flag.Parse() + if *outputfilename == "" || *packageName == "" || *headerFileName == "" || *jsonExampleFileName == "" { + printExit("one or more of the required input arguments was not provided\n") + } + + localDefaultsBytes, err := ioutil.ReadFile(*headerFileName) + if err != nil { + printExit("Unable to load file %s : %v", *headerFileName, err) + } + localDefaultsBytes = []byte(strings.Replace(string(localDefaultsBytes), "{DATE_Y}", fmt.Sprintf("%d", time.Now().Year()), 1)) + localDefaultsBytes = append(localDefaultsBytes, []byte(autoGenHeader)...) + + // add the package name: + localDefaultsBytes = append(localDefaultsBytes, []byte(fmt.Sprintf("\npackage %s\n\n", *packageName))...) + + autoDefaultsBytes := []byte(prettyPrint(config.AutogenLocal, "go")) + + localDefaultsBytes = append(localDefaultsBytes, autoDefaultsBytes...) + + err = ioutil.WriteFile(*outputfilename, localDefaultsBytes, 0644) + if err != nil { + printExit("Unable to write file %s : %v", *outputfilename, err) + } + + // generate an update json for the example as well. + autoDefaultsBytes = []byte(prettyPrint(config.AutogenLocal, "json")) + err = ioutil.WriteFile(*jsonExampleFileName, autoDefaultsBytes, 0644) + if err != nil { + printExit("Unable to write file %s : %v", *jsonExampleFileName, err) + } +} + +type byFieldName []reflect.StructField + +func (a byFieldName) Len() int { return len(a) } +func (a byFieldName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byFieldName) Less(i, j int) bool { + if a[i].Name == "Version" { + return true + } else if a[j].Name == "Version" { + return false + } + return a[i].Name < a[j].Name +} + +func prettyPrint(c config.Local, format string) (out string) { + localType := reflect.TypeOf(c) + + fields := []reflect.StructField{} + for fieldNum := 0; fieldNum < localType.NumField(); fieldNum++ { + fields = append(fields, localType.Field(fieldNum)) + } + + sort.Sort(byFieldName(fields)) + + if format == "go" { + out = fmt.Sprintf("%s = Local{\n", defaultLocalVariableDeclaration) + } else { + out = "{\n" + } + + for fieldIdx, field := range fields { + switch field.Type.Kind() { + case reflect.Bool: + v := reflect.ValueOf(&c).Elem().FieldByName(field.Name).Bool() + if format == "go" { + out = fmt.Sprintf("%s\t%s:\t%v,\n", out, field.Name, v) + } else { + out = fmt.Sprintf("%s \"%s\": %v,\n", out, field.Name, v) + } + case reflect.Int32: + fallthrough + case reflect.Int: + fallthrough + case reflect.Int64: + v := reflect.ValueOf(&c).Elem().FieldByName(field.Name).Int() + if format == "go" { + out = fmt.Sprintf("%s\t%s:\t%d,\n", out, field.Name, v) + } else { + out = fmt.Sprintf("%s \"%s\": %d,\n", out, field.Name, v) + } + case reflect.Uint32: + fallthrough + case reflect.Uint: + fallthrough + case reflect.Uint64: + v := reflect.ValueOf(&c).Elem().FieldByName(field.Name).Uint() + if format == "go" { + out = fmt.Sprintf("%s\t%s:\t%d,\n", out, field.Name, v) + } else { + out = fmt.Sprintf("%s \"%s\": %d,\n", out, field.Name, v) + } + case reflect.String: + v := reflect.ValueOf(&c).Elem().FieldByName(field.Name).String() + if format == "go" { + out = fmt.Sprintf("%s\t%s:\t\"%s\",\n", out, field.Name, v) + } else { + out = fmt.Sprintf("%s \"%s\": \"%s\",\n", out, field.Name, v) + } + case reflect.Map: + if reflect.ValueOf(&c).Elem().FieldByName(field.Name).Len() == 0 { + if format == "go" { + // it's an empty map; good, we know how to initialize empty maps. + mapKeysType := field.Type.Key() + mapValueType := field.Type.Elem() + + out = fmt.Sprintf("%s\t%s:\tmap[%s]%s{},\n", out, field.Name, mapKeysType, mapValueType) + } else { + out = fmt.Sprintf("%s \"%s\": {},\n", out, field.Name) + } + } else { + printExit("non-empty default maps data type encountered when reflecting on config.Local datatype %s", field.Name) + } + default: + printExit("unsupported data type (%s) encountered when reflecting on config.Local datatype %s", field.Type.Kind(), field.Name) + } + if format != "go" { + if fieldIdx == len(fields)-1 { + out = out[:len(out)-2] + "\n" + } + } + } + if format == "go" { + out = out + "}" + } else { + out = out + "}\n" + } + return +} diff --git a/config/local_defaults.go b/config/local_defaults.go index 280d9bd7b8..11624bd49f 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -14,243 +14,56 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package config - -import ( - "fmt" - "time" -) - -var defaultLocal = defaultLocalV9 - -const configVersion = uint32(9) +// This file was auto generated by ./config/defaultsGenerator/defaultsGenerator.go, and SHOULD NOT BE MODIFIED in any way +// If you want to make changes to this file, make the corresponding changes to Local in config.go and run "go generate". -// !!! WARNING !!! -// -// These versioned structures need to be maintained CAREFULLY and treated -// like UNIVERSAL CONSTANTS - they should not be modified once committed. -// -// New fields may be added to the current defaultLocalV# and should -// also be added to installer/config.json.example and -// test/testdata/configs/config-v{n}.json -// -// Changing a default value requires creating a new defaultLocalV# instance, -// bump the version number (configVersion), and add appropriate migration and tests. -// -// !!! WARNING !!! +package config -var defaultLocalV9 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file +var defaultLocal = Local{ Version: 9, + AnnounceParticipationKey: true, Archival: false, BaseLoggerDebugLevel: 4, BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, - PriorityPeers: map[string]bool{}, CadaverSizeTarget: 1073741824, + CatchpointFileHistoryLength: 365, + CatchpointInterval: 10000, + CatchupBlockDownloadRetryAttempts: 1000, CatchupFailurePeerRefreshRate: 10, + CatchupGossipBlockFetchTimeoutSec: 4, + CatchupHTTPBlockFetchTimeoutSec: 4, + CatchupLedgerDownloadRetryAttempts: 50, CatchupParallelBlocks: 16, ConnectionsRateLimitingCount: 60, ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, DNSBootstrapID: ".algorand.network", + DNSSecurityFlags: 1, + DeadlockDetection: 0, + DisableOutgoingConnectionThrottling: false, EnableAgreementReporting: false, EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableRequestLogger: false, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", - LogArchiveMaxAge: "", - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, - ReservedFDs: 256, - RestReadTimeoutSeconds: 15, - RestWriteTimeoutSeconds: 120, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TelemetryToLog: true, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 15000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, - PeerConnectionsUpdateInterval: 3600, - DNSSecurityFlags: 0x01, - EnablePingHandler: true, - CatchpointInterval: 10000, - CatchpointFileHistoryLength: 365, - EnableLedgerService: false, + EnableAssembleStats: false, EnableBlockService: false, EnableGossipBlockService: true, - CatchupHTTPBlockFetchTimeoutSec: 4, // added in V9 - CatchupGossipBlockFetchTimeoutSec: 4, // added in V9 - CatchupLedgerDownloadRetryAttempts: 50, // added in V9 - CatchupBlockDownloadRetryAttempts: 1000, // added in V9 - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV8 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 8, - Archival: false, - BaseLoggerDebugLevel: 4, - BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, - PriorityPeers: map[string]bool{}, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 16, - ConnectionsRateLimitingCount: 60, - ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableRequestLogger: false, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", - LogArchiveMaxAge: "", - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, - ReservedFDs: 256, - RestReadTimeoutSeconds: 15, - RestWriteTimeoutSeconds: 120, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TelemetryToLog: true, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 15000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, - PeerConnectionsUpdateInterval: 3600, - DNSSecurityFlags: 0x01, - EnablePingHandler: true, - CatchpointInterval: 10000, - CatchpointFileHistoryLength: 365, EnableLedgerService: false, - EnableBlockService: false, - EnableGossipBlockService: true, // added in V8 - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV7 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 7, - Archival: false, - BaseLoggerDebugLevel: 4, - BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, - PriorityPeers: map[string]bool{}, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 16, - ConnectionsRateLimitingCount: 60, - ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, EnableMetricReporting: false, EnableOutgoingNetworkMessageFiltering: true, - EnableRequestLogger: false, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", - LogArchiveMaxAge: "", - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, - ReservedFDs: 256, - RestReadTimeoutSeconds: 15, - RestWriteTimeoutSeconds: 120, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TelemetryToLog: true, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 15000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, - PeerConnectionsUpdateInterval: 3600, - DNSSecurityFlags: 0x01, EnablePingHandler: true, - CatchpointInterval: 10000, // added in V7 - CatchpointFileHistoryLength: 365, // add in V7 - EnableLedgerService: false, // added in V7 - EnableBlockService: false, // added in V7 - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV6 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 6, - Archival: false, - BaseLoggerDebugLevel: 4, - BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, - PriorityPeers: map[string]bool{}, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 16, - ConnectionsRateLimitingCount: 60, - ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, + EnableProcessBlockStats: false, + EnableProfiler: false, EnableRequestLogger: false, EnableTopAccountsReporting: false, EndpointAddress: "127.0.0.1:0", + FallbackDNSResolverAddress: "", + ForceRelayMessages: false, GossipFanout: 4, IncomingConnectionsLimit: 10000, IncomingMessageFilterBucketCount: 5, IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", + IsIndexerActive: false, LogArchiveMaxAge: "", + LogArchiveName: "node.archive.log", LogSizeLimit: 1073741824, MaxConnectionsPerIP: 30, NetAddress: "", @@ -259,400 +72,24 @@ var defaultLocalV6 = Local{ NodeExporterPath: "./node_exporter", OutgoingMessageFilterBucketCount: 3, OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, - ReservedFDs: 256, - RestReadTimeoutSeconds: 15, - RestWriteTimeoutSeconds: 120, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TelemetryToLog: true, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 15000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, PeerConnectionsUpdateInterval: 3600, - DNSSecurityFlags: 0x01, // New value with default 0x01 - EnablePingHandler: true, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV5 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 5, - Archival: false, - BaseLoggerDebugLevel: 4, // Was 1 - BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, + PeerPingPeriodSeconds: 0, PriorityPeers: map[string]bool{}, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 16, - ConnectionsRateLimitingCount: 60, - ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, - DisableOutgoingConnectionThrottling: false, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableRequestLogger: false, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, // Was -1 - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", - LogArchiveMaxAge: "", - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - PeerConnectionsUpdateInterval: 3600, - ReconnectTime: 1 * time.Minute, // Was 60ns + PublicAddress: "", + ReconnectTime: 60000000000, ReservedFDs: 256, RestReadTimeoutSeconds: 15, RestWriteTimeoutSeconds: 120, RunHosted: false, SuggestedFeeBlockHistory: 3, SuggestedFeeSlidingWindowSize: 50, + TLSCertFile: "", + TLSKeyFile: "", TelemetryToLog: true, TxPoolExponentialIncreaseFactor: 2, TxPoolSize: 15000, TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, TxSyncServeResponseSize: 1000000, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV4 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 4, - Archival: false, - BaseLoggerDebugLevel: 4, // Was 1 - BroadcastConnectionsLimit: -1, - AnnounceParticipationKey: true, - PriorityPeers: map[string]bool{}, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 50, - ConnectionsRateLimitingCount: 60, - ConnectionsRateLimitingWindowSeconds: 1, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableRequestLogger: false, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, // Was -1 - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogArchiveName: "node.archive.log", - LogArchiveMaxAge: "", - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, // Was 60ns - ReservedFDs: 256, - RestReadTimeoutSeconds: 15, - RestWriteTimeoutSeconds: 120, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 50000, - TxSyncIntervalSeconds: 60, TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, - - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV3 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 3, - Archival: false, - BaseLoggerDebugLevel: 4, // Was 1 - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - CatchupParallelBlocks: 50, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableAgreementReporting: false, - EnableAgreementTimeMetrics: false, - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, // Was -1 - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogSizeLimit: 1073741824, - MaxConnectionsPerIP: 30, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, // Was 60ns - ReservedFDs: 256, - RunHosted: false, - SuggestedFeeBlockHistory: 3, - SuggestedFeeSlidingWindowSize: 50, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 50000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - TxSyncServeResponseSize: 1000000, - IsIndexerActive: false, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV2 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 2, - Archival: false, - BaseLoggerDebugLevel: 4, // Was 1 - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, // Was -1 - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogSizeLimit: 1073741824, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, // Was 60ns - ReservedFDs: 256, - SuggestedFeeBlockHistory: 3, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 50000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV1 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 1, - Archival: false, - BaseLoggerDebugLevel: 4, // Was 1 - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - DeadlockDetection: 0, - DNSBootstrapID: ".algorand.network", - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: 10000, // Was -1 - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogSizeLimit: 1073741824, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 1 * time.Minute, // Was 60ns - SuggestedFeeBlockHistory: 3, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 50000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -var defaultLocalV0 = Local{ - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file - Version: 0, - Archival: false, - BaseLoggerDebugLevel: 1, - CadaverSizeTarget: 1073741824, - CatchupFailurePeerRefreshRate: 10, - DNSBootstrapID: ".algorand.network", - EnableIncomingMessageFilter: false, - EnableMetricReporting: false, - EnableOutgoingNetworkMessageFiltering: true, - EnableTopAccountsReporting: false, - EndpointAddress: "127.0.0.1:0", - GossipFanout: 4, - IncomingConnectionsLimit: -1, - IncomingMessageFilterBucketCount: 5, - IncomingMessageFilterBucketSize: 512, - LogSizeLimit: 1073741824, - NetAddress: "", - NodeExporterListenAddress: ":9100", - NodeExporterPath: "./node_exporter", - OutgoingMessageFilterBucketCount: 3, - OutgoingMessageFilterBucketSize: 128, - ReconnectTime: 60, - SuggestedFeeBlockHistory: 3, - TxPoolExponentialIncreaseFactor: 2, - TxPoolSize: 50000, - TxSyncIntervalSeconds: 60, - TxSyncTimeoutSeconds: 30, - // DO NOT MODIFY VALUES - New values may be added carefully - See WARNING at top of file -} - -func migrate(cfg Local) (newCfg Local, err error) { - newCfg = cfg - if cfg.Version == configVersion { - return - } - - if cfg.Version > configVersion { - err = fmt.Errorf("unexpected config version: %d", cfg.Version) - return - } - - // For now, manually perform migration. - // When we have more time, we can use reflection to migrate from initial - // version to latest version (progressively applying defaults) - // Migrate 0 -> 1 - if newCfg.Version == 0 { - if newCfg.BaseLoggerDebugLevel == defaultLocalV0.BaseLoggerDebugLevel { - newCfg.BaseLoggerDebugLevel = defaultLocalV1.BaseLoggerDebugLevel - } - if newCfg.IncomingConnectionsLimit == defaultLocalV0.IncomingConnectionsLimit { - newCfg.IncomingConnectionsLimit = defaultLocalV1.IncomingConnectionsLimit - } - if newCfg.ReconnectTime == defaultLocalV0.ReconnectTime { - newCfg.ReconnectTime = defaultLocalV1.ReconnectTime - } - newCfg.Version = 1 - } - // Migrate 1 -> 2 - if newCfg.Version == 1 { - if newCfg.ReservedFDs == defaultLocalV1.ReservedFDs { - newCfg.ReservedFDs = defaultLocalV2.ReservedFDs - } - newCfg.Version = 2 - } - // Migrate 2 -> 3 - if newCfg.Version == 2 { - if newCfg.MaxConnectionsPerIP == defaultLocalV2.MaxConnectionsPerIP { - newCfg.MaxConnectionsPerIP = defaultLocalV3.MaxConnectionsPerIP - } - if newCfg.CatchupParallelBlocks == defaultLocalV2.CatchupParallelBlocks { - newCfg.CatchupParallelBlocks = defaultLocalV3.CatchupParallelBlocks - } - newCfg.Version = 3 - } - // Migrate 3 -> 4 - if newCfg.Version == 3 { - if newCfg.BroadcastConnectionsLimit == defaultLocalV3.BroadcastConnectionsLimit { - newCfg.BroadcastConnectionsLimit = defaultLocalV4.BroadcastConnectionsLimit - } - if newCfg.AnnounceParticipationKey == defaultLocalV3.AnnounceParticipationKey { - newCfg.AnnounceParticipationKey = defaultLocalV4.AnnounceParticipationKey - } - if newCfg.PriorityPeers == nil { - newCfg.PriorityPeers = map[string]bool{} - } - newCfg.Version = 4 - } - // Migrate 4 -> 5 - if newCfg.Version == 4 { - if newCfg.TxPoolSize == defaultLocalV4.TxPoolSize { - newCfg.TxPoolSize = defaultLocalV5.TxPoolSize - } - if newCfg.CatchupParallelBlocks == defaultLocalV4.CatchupParallelBlocks { - newCfg.CatchupParallelBlocks = defaultLocalV5.CatchupParallelBlocks - } - if newCfg.PeerConnectionsUpdateInterval == defaultLocalV4.PeerConnectionsUpdateInterval { - newCfg.PeerConnectionsUpdateInterval = defaultLocalV5.PeerConnectionsUpdateInterval - } - - newCfg.Version = 5 - } - - // Migrate 5 -> 6 - if newCfg.Version == 5 { - if newCfg.DNSSecurityFlags == 0 { - newCfg.DNSSecurityFlags = defaultLocalV6.DNSSecurityFlags - } - if newCfg.EnablePingHandler == defaultLocalV5.EnablePingHandler { - newCfg.EnablePingHandler = defaultLocalV6.EnablePingHandler - } - - newCfg.Version = 6 - } - - // Migrate 6 -> 7 - if newCfg.Version == 6 { - if newCfg.CatchpointInterval == defaultLocalV6.CatchpointInterval { - newCfg.CatchpointInterval = defaultLocalV7.CatchpointInterval - } - if newCfg.CatchpointFileHistoryLength == defaultLocalV6.CatchpointFileHistoryLength { - newCfg.CatchpointFileHistoryLength = defaultLocalV7.CatchpointFileHistoryLength - } - - newCfg.Version = 7 - } - - // Migrate 7 -> 8 - if newCfg.Version == 7 { - if newCfg.EnableGossipBlockService == defaultLocalV7.EnableGossipBlockService { - newCfg.EnableGossipBlockService = defaultLocalV8.EnableGossipBlockService - } - - newCfg.Version = 8 - } - - // Migrate 8 -> 9 - if newCfg.Version == 8 { - if newCfg.CatchupHTTPBlockFetchTimeoutSec == defaultLocalV8.CatchupHTTPBlockFetchTimeoutSec { - newCfg.CatchupHTTPBlockFetchTimeoutSec = defaultLocalV9.CatchupHTTPBlockFetchTimeoutSec - } - - if newCfg.CatchupGossipBlockFetchTimeoutSec == defaultLocalV8.CatchupGossipBlockFetchTimeoutSec { - newCfg.CatchupGossipBlockFetchTimeoutSec = defaultLocalV9.CatchupGossipBlockFetchTimeoutSec - } - - if newCfg.CatchupLedgerDownloadRetryAttempts == defaultLocalV8.CatchupLedgerDownloadRetryAttempts { - newCfg.CatchupLedgerDownloadRetryAttempts = defaultLocalV9.CatchupLedgerDownloadRetryAttempts - } - - if newCfg.CatchupBlockDownloadRetryAttempts == defaultLocalV8.CatchupBlockDownloadRetryAttempts { - newCfg.CatchupBlockDownloadRetryAttempts = defaultLocalV9.CatchupBlockDownloadRetryAttempts - } - - newCfg.Version = 9 - } - - if newCfg.Version != configVersion { - err = fmt.Errorf("failed to migrate config version %d (stuck at %d) to latest %d", cfg.Version, newCfg.Version, configVersion) - } - return + UseXForwardedForAddressField: "", } diff --git a/config/migrate.go b/config/migrate.go new file mode 100644 index 0000000000..4b1aa21ba7 --- /dev/null +++ b/config/migrate.go @@ -0,0 +1,200 @@ +// 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 . + +package config + +import ( + "fmt" + "reflect" + "strconv" +) + +//go:generate $GOROOT/bin/go run ./defaultsGenerator/defaultsGenerator.go -h ../scripts/LICENSE_HEADER -p config -o ./local_defaults.go -j ../installer/config.json.example +//go:generate $GOROOT/bin/go fmt local_defaults.go + +// AutogenLocal - this variable is the "input" for the config default generator which automatically updates the above defaultLocal varaible. +// it's implemented in ./config/defaults_gen.go, and should be the only "consumer" of this exported variable +var AutogenLocal = getVersionedDefaultLocalConfig(getLatestConfigVersion()) + +func migrate(cfg Local) (newCfg Local, err error) { + newCfg = cfg + latestConfigVersion := getLatestConfigVersion() + + if cfg.Version > latestConfigVersion { + err = fmt.Errorf("unexpected config version: %d", cfg.Version) + return + } + + for { + if newCfg.Version == latestConfigVersion { + break + } + defaultCurrentConfig := getVersionedDefaultLocalConfig(newCfg.Version) + localType := reflect.TypeOf(Local{}) + nextVersion := newCfg.Version + 1 + for fieldNum := 0; fieldNum < localType.NumField(); fieldNum++ { + field := localType.Field(fieldNum) + nextVersionDefaultValue, hasTag := reflect.StructTag(field.Tag).Lookup(fmt.Sprintf("version[%d]", nextVersion)) + if !hasTag { + continue + } + if nextVersionDefaultValue == "" { + switch reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Kind() { + case reflect.Map: + // if the current implementation have a nil value, use the same value as + // the default one ( i.e. empty map rather than nil map) + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Len() == 0 { + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Set(reflect.MakeMap(field.Type)) + } + case reflect.Array: + // if the current implementation have a nil value, use the same value as + // the default one ( i.e. empty slice rather than nil slice) + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Len() == 0 { + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Set(reflect.MakeSlice(field.Type, 0, 0)) + } + default: + } + continue + } + // we have found a field that has a new value for this new version. See if the current configuration value for that + // field is identical to the default configuration for the field. + switch reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Kind() { + case reflect.Bool: + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Bool() == reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Bool() { + // we're skipping the error checking here since we already tested that in the unit test. + boolVal, _ := strconv.ParseBool(nextVersionDefaultValue) + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).SetBool(boolVal) + } + case reflect.Int32: + fallthrough + case reflect.Int: + fallthrough + case reflect.Int64: + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Int() == reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Int() { + // we're skipping the error checking here since we already tested that in the unit test. + intVal, _ := strconv.ParseInt(nextVersionDefaultValue, 10, 64) + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).SetInt(intVal) + } + case reflect.Uint32: + fallthrough + case reflect.Uint: + fallthrough + case reflect.Uint64: + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).Uint() == reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Uint() { + // we're skipping the error checking here since we already tested that in the unit test. + uintVal, _ := strconv.ParseUint(nextVersionDefaultValue, 10, 64) + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).SetUint(uintVal) + } + case reflect.String: + if reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).String() == reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).String() { + // we're skipping the error checking here since we already tested that in the unit test. + reflect.ValueOf(&newCfg).Elem().FieldByName(field.Name).SetString(nextVersionDefaultValue) + } + default: + panic(fmt.Sprintf("unsupported data type (%s) encountered when reflecting on config.Local datatype %s", reflect.ValueOf(&defaultCurrentConfig).Elem().FieldByName(field.Name).Kind(), field.Name)) + } + } + } + return +} + +func getLatestConfigVersion() uint32 { + localType := reflect.TypeOf(Local{}) + versionField, found := localType.FieldByName("Version") + if !found { + return 0 + } + version := uint32(0) + for { + _, hasTag := reflect.StructTag(versionField.Tag).Lookup(fmt.Sprintf("version[%d]", version+1)) + if !hasTag { + return version + } + version++ + } +} + +func getVersionedDefaultLocalConfig(version uint32) (local Local) { + if version < 0 { + return + } + if version > 0 { + local = getVersionedDefaultLocalConfig(version - 1) + } + // apply version specific changes. + localType := reflect.TypeOf(local) + for fieldNum := 0; fieldNum < localType.NumField(); fieldNum++ { + field := localType.Field(fieldNum) + versionDefaultValue, hasTag := reflect.StructTag(field.Tag).Lookup(fmt.Sprintf("version[%d]", version)) + if !hasTag { + continue + } + if versionDefaultValue == "" { + // set the default field value in case it's a map/array so we won't have nil ones. + switch reflect.ValueOf(&local).Elem().FieldByName(field.Name).Kind() { + case reflect.Map: + reflect.ValueOf(&local).Elem().FieldByName(field.Name).Set(reflect.MakeMap(field.Type)) + case reflect.Array: + reflect.ValueOf(&local).Elem().FieldByName(field.Name).Set(reflect.MakeSlice(field.Type, 0, 0)) + default: + } + continue + } + switch reflect.ValueOf(&local).Elem().FieldByName(field.Name).Kind() { + case reflect.Bool: + boolVal, err := strconv.ParseBool(versionDefaultValue) + if err != nil { + panic(err) + } + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetBool(boolVal) + + case reflect.Int32: + intVal, err := strconv.ParseInt(versionDefaultValue, 10, 32) + if err != nil { + panic(err) + } + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetInt(intVal) + case reflect.Int: + fallthrough + case reflect.Int64: + intVal, err := strconv.ParseInt(versionDefaultValue, 10, 64) + if err != nil { + panic(err) + } + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetInt(intVal) + + case reflect.Uint32: + uintVal, err := strconv.ParseUint(versionDefaultValue, 10, 32) + if err != nil { + panic(err) + } + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetUint(uintVal) + case reflect.Uint: + fallthrough + case reflect.Uint64: + uintVal, err := strconv.ParseUint(versionDefaultValue, 10, 64) + if err != nil { + panic(err) + } + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetUint(uintVal) + case reflect.String: + reflect.ValueOf(&local).Elem().FieldByName(field.Name).SetString(versionDefaultValue) + default: + panic(fmt.Sprintf("unsupported data type (%s) encountered when reflecting on config.Local datatype %s", reflect.ValueOf(&local).Elem().FieldByName(field.Name).Kind(), field.Name)) + } + } + return +} diff --git a/installer/config.json.example b/installer/config.json.example index 18ae2d952e..0c156580fe 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -5,27 +5,36 @@ "BaseLoggerDebugLevel": 4, "BroadcastConnectionsLimit": -1, "CadaverSizeTarget": 1073741824, + "CatchpointFileHistoryLength": 365, + "CatchpointInterval": 10000, "CatchupBlockDownloadRetryAttempts": 1000, "CatchupFailurePeerRefreshRate": 10, "CatchupGossipBlockFetchTimeoutSec": 4, "CatchupHTTPBlockFetchTimeoutSec": 4, "CatchupLedgerDownloadRetryAttempts": 50, "CatchupParallelBlocks": 16, - "CatchpointInterval": 10000, - "CatchpointFileHistoryLength": 365, "ConnectionsRateLimitingCount": 60, "ConnectionsRateLimitingWindowSeconds": 1, - "DisableOutgoingConnectionThrottling": false, - "DeadlockDetection": 0, "DNSBootstrapID": ".algorand.network", + "DNSSecurityFlags": 1, + "DeadlockDetection": 0, + "DisableOutgoingConnectionThrottling": false, + "EnableAgreementReporting": false, + "EnableAgreementTimeMetrics": false, + "EnableAssembleStats": false, + "EnableBlockService": false, "EnableGossipBlockService": true, "EnableIncomingMessageFilter": false, + "EnableLedgerService": false, "EnableMetricReporting": false, "EnableOutgoingNetworkMessageFiltering": true, + "EnablePingHandler": true, + "EnableProcessBlockStats": false, "EnableProfiler": false, "EnableRequestLogger": false, "EnableTopAccountsReporting": false, "EndpointAddress": "127.0.0.1:0", + "FallbackDNSResolverAddress": "", "ForceRelayMessages": false, "GossipFanout": 4, "IncomingConnectionsLimit": 10000, @@ -37,12 +46,15 @@ "LogSizeLimit": 1073741824, "MaxConnectionsPerIP": 30, "NetAddress": "", + "NetworkProtocolVersion": "", "NodeExporterListenAddress": ":9100", "NodeExporterPath": "./node_exporter", "OutgoingMessageFilterBucketCount": 3, "OutgoingMessageFilterBucketSize": 128, "PeerConnectionsUpdateInterval": 3600, + "PeerPingPeriodSeconds": 0, "PriorityPeers": {}, + "PublicAddress": "", "ReconnectTime": 60000000000, "ReservedFDs": 256, "RestReadTimeoutSeconds": 15, @@ -50,13 +62,13 @@ "RunHosted": false, "SuggestedFeeBlockHistory": 3, "SuggestedFeeSlidingWindowSize": 50, + "TLSCertFile": "", + "TLSKeyFile": "", "TelemetryToLog": true, "TxPoolExponentialIncreaseFactor": 2, "TxPoolSize": 15000, "TxSyncIntervalSeconds": 60, "TxSyncServeResponseSize": 1000000, "TxSyncTimeoutSeconds": 30, - "PeerConnectionsUpdateInterval": 3600, - "DNSSecurityFlags": 1, - "EnablePingHandler": true + "UseXForwardedForAddressField": "" } From 97143e585d801d48df824da948d99dbd0ab9d4cf Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 22 Jun 2020 15:44:19 -0400 Subject: [PATCH 069/267] Add EnableDeveloperAPI configuration flag (#1191) Add a local config to disable teal/compile in algod by default. --- config/config.go | 4 ++++ config/local_defaults.go | 1 + daemon/algod/api/server/v2/handlers.go | 5 +++++ daemon/algod/api/server/v2/test/handlers_test.go | 10 ++++++---- daemon/algod/api/server/v2/test/helpers.go | 5 +++-- installer/config.json.example | 1 + 6 files changed, 20 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index eb83f13b33..8a9084c31e 100644 --- a/config/config.go +++ b/config/config.go @@ -317,6 +317,10 @@ type Local struct { // CatchupLedgerDownloadRetryAttempts controls the number of attempt the block fetching would be attempted before giving up catching up to the provided catchpoint. CatchupBlockDownloadRetryAttempts int `version[9]:"1000"` + + // EnableDeveloperAPI enables teal/compile, teal/dryrun API endpoints. + // This functionlity is disabled by default. + EnableDeveloperAPI bool `version[9]:"false"` } // Filenames of config files within the configdir (e.g. ~/.algorand) diff --git a/config/local_defaults.go b/config/local_defaults.go index 11624bd49f..9dc909a8f3 100644 --- a/config/local_defaults.go +++ b/config/local_defaults.go @@ -44,6 +44,7 @@ var defaultLocal = Local{ EnableAgreementTimeMetrics: false, EnableAssembleStats: false, EnableBlockService: false, + EnableDeveloperAPI: false, EnableGossipBlockService: true, EnableIncomingMessageFilter: false, EnableLedgerService: false, diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index ab06adc081..a0f251dd8a 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -64,6 +64,7 @@ type NodeInterface interface { SuggestedFee() basics.MicroAlgos StartCatchup(catchpoint string) error AbortCatchup(catchpoint string) error + Config() config.Local } // RegisterParticipationKeys registers participation keys. @@ -589,6 +590,10 @@ func (v2 *Handlers) AbortCatchup(ctx echo.Context, catchpoint string) error { // TealCompile compiles TEAL code to binary, return both binary and hash // (POST /v2/teal/compile) func (v2 *Handlers) TealCompile(ctx echo.Context) error { + // return early if teal compile is not allowed in node config + if !v2.Node.Config().EnableDeveloperAPI { + return ctx.String(http.StatusNotFound, "/teal/compile was not enabled in the configuration file by setting the EnableDeveloperAPI to true") + } buf := new(bytes.Buffer) ctx.Request().Body = http.MaxBytesReader(nil, ctx.Request().Body, maxTealSourceBytes) buf.ReadFrom(ctx.Request().Body) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 51f6f716a3..6f023fdf0f 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -293,7 +293,7 @@ func TestAbortCatchup(t *testing.T) { abortCatchupTest(t, badCatchPoint, 400) } -func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int) { +func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int, enableDeveloperAPI bool) { numAccounts := 1 numTransactions := 1 offlineAccounts := true @@ -301,6 +301,7 @@ func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int) { defer releasefunc() dummyShutdownChan := make(chan struct{}) mockNode := makeMockNode(mockLedger, t.Name()) + mockNode.config.EnableDeveloperAPI = enableDeveloperAPI handler := v2.Handlers{ Node: &mockNode, Log: logging.Base(), @@ -316,11 +317,12 @@ func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int) { } func TestTealCompile(t *testing.T) { - tealCompileTest(t, nil, 200) // nil program should work + tealCompileTest(t, nil, 200, true) // nil program should work goodProgram := `int 1` goodProgramBytes := []byte(goodProgram) - tealCompileTest(t, goodProgramBytes, 200) + tealCompileTest(t, goodProgramBytes, 200, true) + tealCompileTest(t, goodProgramBytes, 404, false) badProgram := "bad program" badProgramBytes := []byte(badProgram) - tealCompileTest(t, badProgramBytes, 400) + tealCompileTest(t, badProgramBytes, 400, true) } diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 3d017acaff..4617da6d8c 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -74,10 +74,11 @@ var poolAddrResponseGolden = generatedV2.AccountResponse{ type mockNode struct { ledger *data.Ledger genesisID string + config config.Local } func makeMockNode(ledger *data.Ledger, genesisID string) mockNode { - return mockNode{ledger: ledger, genesisID: genesisID} + return mockNode{ledger: ledger, genesisID: genesisID, config: config.GetDefaultLocal()} } func (m mockNode) Ledger() *data.Ledger { @@ -116,7 +117,7 @@ func (m mockNode) SuggestedFee() basics.MicroAlgos { // unused by handlers: func (m mockNode) Config() config.Local { - return config.GetDefaultLocal() + return m.config } func (m mockNode) Start() {} diff --git a/installer/config.json.example b/installer/config.json.example index 0c156580fe..4eb867ed76 100644 --- a/installer/config.json.example +++ b/installer/config.json.example @@ -23,6 +23,7 @@ "EnableAgreementTimeMetrics": false, "EnableAssembleStats": false, "EnableBlockService": false, + "EnableDeveloperAPI": false, "EnableGossipBlockService": true, "EnableIncomingMessageFilter": false, "EnableLedgerService": false, From 5a074a263939d4fe50fd56e57ae77e8d20538f9c Mon Sep 17 00:00:00 2001 From: btoll Date: Mon, 22 Jun 2020 21:32:23 -0400 Subject: [PATCH 070/267] Fix deploy location for latest rpm package (#1182) For now, put all packages in the stable directory in the algorand-releases bucket for the rpm repo. This also fixes a typo in a mule task name. --- package-deploy.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/package-deploy.yaml b/package-deploy.yaml index 514f078031..53ee240aac 100644 --- a/package-deploy.yaml +++ b/package-deploy.yaml @@ -53,12 +53,12 @@ tasks: - task: s3.BucketCopy name: deb src: s3://algorand-staging/releases/$CHANNEL/$VERSION/algorand_${CHANNEL}_${OS_TYPE}-${ARCH_TYPE}_${VERSION}.deb - dest: /projects/go-algorand/tmp/node_pkgs/${OS_TYPE}/${ARCH_TYPE} + dest: /projects/go-algorand/tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE - task: s3.BucketCopy name: rpm src: s3://algorand-staging/releases/$CHANNEL/$VERSION/algorand-${VERSION}-1.${ARCH_BIT}.rpm - dest: /projects/go-algorand/tmp/node_pkgs/${OS_TYPE}/${ARCH_TYPE} + dest: /projects/go-algorand/tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE - task: s3.BucketCopy name: gnupg @@ -67,13 +67,15 @@ tasks: - task: s3.BucketCopy name: deploy-dev-deb-repo - src: s3://algorand-staging/releases/${CHANNEL}/${VERSION} - dest: s3://algorand-dev-deb-repo/releases/${CHANNEL}/${VERSION} + src: s3://algorand-staging/releases/$CHANNEL/$VERSION + dest: s3://algorand-dev-deb-repo/releases/$CHANNEL/$VERSION + # TODO: For now, we're hardcoding the channel until the beta + # releases are sorted out. This will then be updated. - task: s3.BucketCopy name: deploy-rpm-repo src: /root/rpmrepo - dest: s3://algorand-releases/rpm/${CHANNEL}/${VERSION} + dest: s3://algorand-releases/rpm/stable jobs: package-deploy: @@ -103,5 +105,5 @@ jobs: package-deploy-rpm-repo: tasks: - - s3.BucketCopy.rpm-repo + - s3.BucketCopy.deploy-rpm-repo From b509cad58cd3fbc2f7efbb2d6fa7ac8c8eef07bd Mon Sep 17 00:00:00 2001 From: wjurayj-algo <65556361+wjurayj-algo@users.noreply.github.com> Date: Wed, 24 Jun 2020 10:40:54 -0400 Subject: [PATCH 071/267] Algoh fix (#1183) Added algod client health check to prevent node shutdown due to timeout in healthy node. --- cmd/algoh/client.go | 1 + cmd/algoh/deadman.go | 43 +++-- cmd/algoh/main.go | 22 +-- cmd/algoh/mockClient.go | 11 ++ .../cli/algoh/expect/algohExpectCommon.exp | 109 ++++++++++++ .../cli/algoh/expect/algohTimeoutTest.exp | 87 ++++++++++ .../cli/algoh/expect/algoh_expect_test.go | 158 ++++++++++++++++++ .../expect/disabled_profiler_config.json | 1 + test/e2e-go/cli/algoh/expect/host-config.json | 1 + 9 files changed, 413 insertions(+), 20 deletions(-) create mode 100644 test/e2e-go/cli/algoh/expect/algohExpectCommon.exp create mode 100644 test/e2e-go/cli/algoh/expect/algohTimeoutTest.exp create mode 100644 test/e2e-go/cli/algoh/expect/algoh_expect_test.go create mode 100644 test/e2e-go/cli/algoh/expect/disabled_profiler_config.json create mode 100644 test/e2e-go/cli/algoh/expect/host-config.json diff --git a/cmd/algoh/client.go b/cmd/algoh/client.go index 07b1b9c769..56b23a0043 100644 --- a/cmd/algoh/client.go +++ b/cmd/algoh/client.go @@ -28,4 +28,5 @@ type Client interface { Status() (generatedV2.NodeStatusResponse, error) Block(round uint64) (v1.Block, error) GetGoRoutines(ctx context.Context) (string, error) + HealthCheck() error } diff --git a/cmd/algoh/deadman.go b/cmd/algoh/deadman.go index 33320f7306..ef1654c9f1 100644 --- a/cmd/algoh/deadman.go +++ b/cmd/algoh/deadman.go @@ -22,6 +22,7 @@ import ( "sync" "time" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/logging/telemetryspec" ) @@ -33,9 +34,10 @@ type deadManWatcher struct { client Client done <-chan struct{} wg *sync.WaitGroup + algodConfig config.Local } -func makeDeadManWatcher(timeout int64, client Client, uploadOnError bool, done <-chan struct{}, wg *sync.WaitGroup) deadManWatcher { +func makeDeadManWatcher(timeout int64, client Client, uploadOnError bool, done <-chan struct{}, wg *sync.WaitGroup, algodConfig config.Local) deadManWatcher { var deadManTime time.Duration if timeout == 0 { deadManTime = time.Hour * (10 * 365 * 24) // Don't fire for 10 years @@ -50,6 +52,7 @@ func makeDeadManWatcher(timeout int64, client Client, uploadOnError bool, done < uploadOnError: uploadOnError, done: done, wg: wg, + algodConfig: algodConfig, } } @@ -90,15 +93,27 @@ func (w deadManWatcher) onBlock(block v1.Block) { } func (w deadManWatcher) reportDeadManTimeout(curBlock uint64) (err error) { - goRoutines, err := getGoRoutines(w.client) - if err != nil { - goRoutines = fmt.Sprintf("Error dumping goroutines: %v", err) - } - - details := telemetryspec.DeadManTriggeredEventDetails{ - Timeout: int64(w.timeout.Seconds()), - CurrentBlock: curBlock, - GoRoutines: goRoutines, + var details telemetryspec.DeadManTriggeredEventDetails + if w.algodConfig.EnableProfiler { + goRoutines, err := getGoRoutines(w.client) + if err != nil { + goRoutines = fmt.Sprintf("Error dumping goroutines: %v", err) + } + details = telemetryspec.DeadManTriggeredEventDetails{ + Timeout: int64(w.timeout.Seconds()), + CurrentBlock: curBlock, + GoRoutines: goRoutines, + } + } else { + healthCheck, err := getHealthCheck(w.client) + if err != nil { + healthCheck = fmt.Sprintf("Error performing health check : %v", err) + } + details = telemetryspec.DeadManTriggeredEventDetails{ + Timeout: int64(w.timeout.Seconds()), + CurrentBlock: curBlock, + GoRoutines: healthCheck, + } } log.EventWithDetails(telemetryspec.HostApplicationState, telemetryspec.DeadManTriggeredEvent, details) @@ -117,3 +132,11 @@ func getGoRoutines(client Client) (goRoutines string, err error) { } return } + +func getHealthCheck(client Client) (healthCheck string, err error) { + err = client.HealthCheck() + if err == nil { + healthCheck = "Node is healthy" + } + return +} diff --git a/cmd/algoh/main.go b/cmd/algoh/main.go index 04bcdcb4f3..bed536e118 100644 --- a/cmd/algoh/main.go +++ b/cmd/algoh/main.go @@ -100,6 +100,12 @@ func main() { reportErrorf("Can't convert data directory's path to absolute, %v\n", dataDir) } + algodConfig, err := config.LoadConfigFromDisk(absolutePath) + + if err != nil && !os.IsNotExist(err) { + log.Fatalf("Cannot load config: %v", err) + } + if _, err := os.Stat(absolutePath); err != nil { reportErrorf("Data directory %s does not appear to be valid\n", dataDir) } @@ -112,7 +118,7 @@ func main() { done := make(chan struct{}) log := logging.Base() - configureLogging(genesis, log, absolutePath, done) + configureLogging(genesis, log, absolutePath, done, algodConfig) defer log.CloseTelemetry() exeDir, err = util.ExeDir() @@ -175,7 +181,7 @@ func main() { var wg sync.WaitGroup - deadMan := makeDeadManWatcher(algohConfig.DeadManTimeSec, algodClient, algohConfig.UploadOnError, done, &wg) + deadMan := makeDeadManWatcher(algohConfig.DeadManTimeSec, algodClient, algohConfig.UploadOnError, done, &wg, algodConfig) wg.Add(1) listeners := []blockListener{deadMan} @@ -259,7 +265,7 @@ func getNodeController() nodecontrol.NodeController { return nc } -func configureLogging(genesis bookkeeping.Genesis, log logging.Logger, rootPath string, abort chan struct{}) { +func configureLogging(genesis bookkeeping.Genesis, log logging.Logger, rootPath string, abort chan struct{}, algodConfig config.Local) { log = logging.Base() liveLog := fmt.Sprintf("%s/host.log", rootPath) @@ -272,7 +278,7 @@ func configureLogging(genesis bookkeeping.Genesis, log logging.Logger, rootPath log.SetJSONFormatter() log.SetLevel(logging.Debug) - initTelemetry(genesis, log, rootPath, abort) + initTelemetry(genesis, log, rootPath, abort, algodConfig) // if we have the telemetry enabled, we want to use it's sessionid as part of the // collected metrics decorations. @@ -281,7 +287,7 @@ func configureLogging(genesis bookkeeping.Genesis, log logging.Logger, rootPath fmt.Fprintln(writer, "++++++++++++++++++++++++++++++++++++++++") } -func initTelemetry(genesis bookkeeping.Genesis, log logging.Logger, dataDirectory string, abort chan struct{}) { +func initTelemetry(genesis bookkeeping.Genesis, log logging.Logger, dataDirectory string, abort chan struct{}, algodConfig config.Local) { // Enable telemetry hook in daemon to send logs to cloud // If ALGOTEST env variable is set, telemetry is disabled - allows disabling telemetry for tests isTest := os.Getenv("ALGOTEST") != "" @@ -304,14 +310,10 @@ func initTelemetry(genesis bookkeeping.Genesis, log logging.Logger, dataDirector } if log.GetTelemetryEnabled() { - cfg, err := config.LoadConfigFromDisk(dataDirectory) - if err != nil && !os.IsNotExist(err) { - log.Fatalf("Cannot load config: %v", err) - } // If the telemetry URI is not set, periodically check SRV records for new telemetry URI if log.GetTelemetryURI() == "" { - network.StartTelemetryURIUpdateService(time.Minute, cfg, genesis.Network, log, abort) + network.StartTelemetryURIUpdateService(time.Minute, algodConfig, genesis.Network, log, abort) } // For privacy concerns, we don't want to provide the full data directory to telemetry. diff --git a/cmd/algoh/mockClient.go b/cmd/algoh/mockClient.go index ce379ca36a..209fda2fa4 100644 --- a/cmd/algoh/mockClient.go +++ b/cmd/algoh/mockClient.go @@ -50,6 +50,7 @@ type mockClient struct { StatusCalls int BlockCalls map[uint64]int GetGoRoutinesCalls int + HealthCheckCalls int error []error status []generatedV2.NodeStatusResponse routine []string @@ -111,3 +112,13 @@ func (c *mockClient) GetGoRoutines(ctx context.Context) (r string, e error) { e = c.nextError() return } + +func (c *mockClient) HealthCheck() (e error) { + c.HealthCheckCalls++ + // Repeat last healthcheck... + if len(c.routine) > 1 { + c.routine = c.routine[1:] + } + e = c.nextError() + return +} diff --git a/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp b/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp new file mode 100644 index 0000000000..0fb4dec439 --- /dev/null +++ b/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp @@ -0,0 +1,109 @@ +# Algoh Expect Utility Package +namespace eval ::Algoh { + + # Export Procedures + + namespace export Abort + namespace export Info + namespace export CreateNetwork + + namespace export StopNode + namespace export StartNode + + + # My Variables + set version 1.0 + set description "Algodh Expect Package" + + # Variable for the path of the script + variable home [file join [pwd] [file dirname [info script]]] +} + +# Definition of the procedure MyProcedure +proc ::Algoh::Info {} { + puts Algoh::description +} + +proc ::Algoh::Abort { ERROR } { + puts "Aborting with error: $ERROR" + exit 1 +} + +package provide Algoh $Algoh::version +package require Tcl 8.0 + + +# Start the network +proc ::Algoh::CreateNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ROOT_DIR } { + + # Running on ARM64, it seems that network creation is pretty slow. + # 30 second won't be enough here, so I'm changing this to 90 seconds. + set timeout 90 + + if { [catch { + # Create network + puts "network create $NETWORK_NAME" + spawn goal network create --network $NETWORK_NAME --template $NETWORK_TEMPLATE --rootdir $TEST_ROOT_DIR + expect { + timeout { close; ::Algoh::Abort "Timed out creating network" } + "^Network $NETWORK_NAME created under.*" { puts "Network $NETWORK_NAME created" ; close } + eof { catch wait result; if { [lindex $result 3] != 0 } { puts "Unable to create network"; Algoh::Abort } } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in CreateNetwork: $EXCEPTION" + } +} + +proc ::Algoh::StartNode { TEST_ALGO_DIR } { + + puts "Primary node start" + # Start node + if { [catch { + spawn goal node start -d $TEST_ALGO_DIR + expect { + timeout { close; ::Algoh::Abort "Timed out starting relay node" } + "^Algorand node successfully started!* { puts "Primary relay node started"; close } + eof { catch wait result; if { [lindex $result 3] != 0 } { puts "Unable to start node"; Algoh::Abort } } + } + } EXCEPTION ] } { + ::Algoh::Abort "ERROR in StartNode: $EXCEPTION" + } +} + +proc ::Algoh::StopNode { TEST_ALGO_DIR } { + set timeout 15 + + if { [catch { + puts "node stop with $TEST_ALGO_DIR" + spawn goal node stop -d $TEST_ALGO_DIR + expect { + timeout { close; ::Algoh::Abort "Did not recieve appropriate message during node stop"} + "*The node was successfully stopped.*" {puts "Node stopped successfully"; close} + eof { catch wait result; if { [lindex $result 3] != 0 } { puts "Unable to stop node"; Algoh::Abort } } + + } + } EXCEPTION] } { + ::Algoh::Abort "ERROR in StopNode: $EXCEPTION" + } +} + +# Stop the network +proc ::Algoh::StopNetwork { NETWORK_NAME TEST_ROOT_DIR } { + set timeout 60 + set NETWORK_STOP_MESSAGE "" + puts "Stopping network: $NETWORK_NAME" + spawn goal network stop -r $TEST_ROOT_DIR + expect { + timeout { + close + puts "Timed out shutting down network" + puts "TEST_ROOT_DIR $::TEST_ROOT_DIR" + puts "NETWORK_NAME $::NETWORK_NAME" + exit 1 + } + "Network Stopped under.*" {set NETWORK_STOP_MESSAGE $expect_out(buffer); close} + eof { catch wait result; if { [lindex $result 3] != 0 } { puts "Unable to stop network"; Algoh::Abort } } + + } + puts $NETWORK_STOP_MESSAGE +} diff --git a/test/e2e-go/cli/algoh/expect/algohTimeoutTest.exp b/test/e2e-go/cli/algoh/expect/algohTimeoutTest.exp new file mode 100644 index 0000000000..20716bef85 --- /dev/null +++ b/test/e2e-go/cli/algoh/expect/algohTimeoutTest.exp @@ -0,0 +1,87 @@ +#!/usr/bin/expect -f +set err 0 +log_user 1 + + + +if { [catch { + source algohExpectCommon.exp + + set TEST_ALGO_DIR [lindex $argv 0] + set TEST_DATA_DIR [lindex $argv 1] + + exec mkdir -p $TEST_ALGO_DIR + + set TIME_STAMP [clock seconds] + set timeout 5 + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary + set TEST_NODE_DIR $TEST_ROOT_DIR/Node + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50Each.json" + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + + + ::Algoh::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ROOT_DIR + + ::Algoh::StartNode $TEST_PRIMARY_NODE_DIR + + exec cat ./disabled_profiler_config.json > $TEST_NODE_DIR/config.json + exec cat ./host-config.json > $TEST_NODE_DIR/host-config.json + + set PRIMARY_ADDR "" + spawn cat $TEST_ROOT_DIR/Primary/algod-listen.net + expect { + -regexp {http://[0-9\.]+:[0-9]+} { regexp -- {[0-9.]+:[0-9]+} $expect_out(0,string) PRIMARY_ADDR; close;} + timeout {puts "missed our case"; close; exit 1} + } + + puts "regex match: $PRIMARY_ADDR" + + #start hosted node in the background + spawn $env(GOPATH)/bin/algoh -d $TEST_NODE_DIR -p $PRIMARY_ADDR + expect { + "^Logging to: *" {puts "algoh startup successful"} + timeout {puts "algoh failed to start"; close; exit 1} + } + + #allow algoh time to put files in Node dir + spawn sleep 5 + set timeout 5 + expect { + timeout {puts "algod should be fully running"; close} + } + + #wait until Node approves blocks to the network + set timeout 60 + spawn $env(GOPATH)/bin/goal node wait -d $TEST_NODE_DIR -w 61 + expect { + eof {puts "successfully communicating with relay node"} + "Timed out waiting for node to make progress" {puts "timed out waiting for connection to relay node"; close; exit 1} + timeout {puts "should not reached this case"; close; exit 1} + } + + + ::Algoh::StopNode $TEST_PRIMARY_NODE_DIR + + + set timeout 201 + spawn $env(GOPATH)/bin/goal node wait -d $TEST_NODE_DIR -w 200 + expect { + "^Cannot contact Algorand node: open $TEST_NODE_DIR/algod.net: no such file or directory." {puts "ERROR: node shutdown"; close; exit 1} + "^Timed out waiting for node to make progress" {puts "node correctly continued running despite relay shutdown"; close} + timeout {puts "should not reached this case", close; exit 1} + } + + ::Algoh::StopNetwork $NETWORK_NAME $TEST_ROOT_DIR + + exec rm -d -r -f $TEST_ALGO_DIR + puts "Basic Algoh Test Successful" + exit 0 +} EXCEPTION ] } { + puts "ERROR in algoh test: $EXCEPTION" + + exec rm -d -r -f $TEST_ALGO_DIR + exit 1 +} diff --git a/test/e2e-go/cli/algoh/expect/algoh_expect_test.go b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go new file mode 100644 index 0000000000..adb17abbfe --- /dev/null +++ b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go @@ -0,0 +1,158 @@ +// 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 . + +package expect + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +type algohExpectFixture struct { + testDir string + testDataDir string + testDirTmp bool + t *testing.T + testFilter string +} + +func (f *algohExpectFixture) initialize(t *testing.T) (err error) { + f.t = t + f.testDir = os.Getenv("TESTDIR") + if f.testDir == "" { + f.testDir, _ = ioutil.TempDir("", "tmp") + f.testDir = filepath.Join(f.testDir, "expect") + err = os.MkdirAll(f.testDir, 0755) + if err != nil { + f.t.Errorf("error creating test dir %s, with error %v", f.testDir, err) + return + } + f.testDirTmp = true + } + f.testDataDir = os.Getenv("TESTDATADIR") + if f.testDataDir == "" { + f.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") + } + + f.testFilter = os.Getenv("TESTFILTER") + if f.testFilter == "" { + f.testFilter = ".*" + } + return +} + +func (f *algohExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { + testName = strings.Replace(testName, ".exp", "", -1) + workingDir = filepath.Join(f.testDir, testName) + err = os.Mkdir(workingDir, 0755) + if err != nil { + f.t.Errorf("error creating test dir %s, with error %v", workingDir, err) + return + } + algoDir = filepath.Join(workingDir, "algoh") + err = os.Mkdir(algoDir, 0755) + if err != nil { + f.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) + return + } + return +} + +func (f *algohExpectFixture) removeTestDir(workingDir string) (err error) { + err = os.RemoveAll(workingDir) + if err != nil { + f.t.Errorf("error removing test dir %s, with error %v", workingDir, err) + return + } + return +} + +// TesthWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algoh/expect directory +func TestAlgohWithExpect(t *testing.T) { + var f algohExpectFixture + var execCommand = exec.Command + expectFiles := make(map[string]string) // map expect test to full file name. + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(info.Name(), "Test.exp") { + expectFiles[info.Name()] = path + } + return nil + }) + require.NoError(t, err) + err = f.initialize(t) + require.NoError(t, err) + + for testName := range expectFiles { + if match, _ := regexp.MatchString(f.testFilter, testName); match { + t.Run(testName, func(t *testing.T) { + workingDir, algoDir, err := f.getTestDir(testName) + require.NoError(t, err) + t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, f.testDataDir) + cmd := execCommand("expect", testName, algoDir, f.testDataDir) + var outBuf bytes.Buffer + cmd.Stdout = &outBuf + + // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor + // attaches goroutine reading that blocks on io.Copy from stderr. + // Cmd::CombinedOutput sets stderr to stdout and also blocks. + // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. + // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. + // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. + // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. + errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) + if err != nil { + t.Logf("failed opening stderr temp file: %s\n", err.Error()) + t.Fail() + } + defer errFile.Close() // Close might error but we Sync it before leaving the scope + cmd.Stderr = errFile + + err = cmd.Run() + if err != nil { + var stderr string + var ferr error + if ferr = errFile.Sync(); ferr == nil { + if _, ferr = errFile.Seek(0, 0); ferr == nil { + if info, ferr := errFile.Stat(); ferr == nil { + errData := make([]byte, info.Size()) + if _, ferr = errFile.Read(errData); ferr == nil { + stderr = string(errData) + } + } + } + } + if ferr != nil { + stderr = ferr.Error() + } + t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) + t.Fail() + } else { + // t.Logf("stdout: %s", string(outBuf.Bytes())) + f.removeTestDir(workingDir) + } + }) + } + } +} diff --git a/test/e2e-go/cli/algoh/expect/disabled_profiler_config.json b/test/e2e-go/cli/algoh/expect/disabled_profiler_config.json new file mode 100644 index 0000000000..f299ebfe04 --- /dev/null +++ b/test/e2e-go/cli/algoh/expect/disabled_profiler_config.json @@ -0,0 +1 @@ +{ "GossipFanout": 1, "EndpointAddress": "127.0.0.1:0", "DNSBootstrapID": "", "EnableProfiler": false, "IncomingConnectionsLimit": 0, "RunHosted": true } \ No newline at end of file diff --git a/test/e2e-go/cli/algoh/expect/host-config.json b/test/e2e-go/cli/algoh/expect/host-config.json new file mode 100644 index 0000000000..a9391f8897 --- /dev/null +++ b/test/e2e-go/cli/algoh/expect/host-config.json @@ -0,0 +1 @@ +{ "DeadManTimeSec": 32, "StatusDelayMS": 45000, "StallDelayMS": 60000, "UploadOnError": true, "SendBlockStats": false } \ No newline at end of file From b02ff6694c670191a4a7751f89d5ee066950a46e Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 24 Jun 2020 13:04:33 -0400 Subject: [PATCH 072/267] Fix route path to use echo parameter notation. (#1193) When the route path was converted from Gorilla Mux notation to Echo one of the parameters was missed. --- daemon/algod/api/server/router_test.go | 182 ++++++++++++++++++++ daemon/algod/api/server/v1/routes/routes.go | 2 +- 2 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 daemon/algod/api/server/router_test.go diff --git a/daemon/algod/api/server/router_test.go b/daemon/algod/api/server/router_test.go new file mode 100644 index 0000000000..ebab8c1682 --- /dev/null +++ b/daemon/algod/api/server/router_test.go @@ -0,0 +1,182 @@ +// 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 . +package server + +import ( + "net/http" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/algorand/go-algorand/daemon/algod/api/server/lib" + "github.com/algorand/go-algorand/daemon/algod/api/server/v1/routes" +) + +type TestSuite struct { + suite.Suite + calls int + e *echo.Echo +} + +func (s *TestSuite) SetupSuite() { + s.e = echo.New() + handler := func(context lib.ReqContext, context2 echo.Context) { + s.calls++ + } + // Make a deep copy of the routes array with dummy handlers that log a call. + v1RoutesCopy := make([]lib.Route, len(routes.V1Routes)) + for _, route := range routes.V1Routes { + v1RoutesCopy = append(v1RoutesCopy, lib.Route{ + Name: route.Name, + Method: route.Method, + Path: route.Path, + HandlerFunc: handler, + }) + } + // Registering v1 routes + registerHandlers(s.e, apiV1Tag, v1RoutesCopy, lib.ReqContext{}) +} +func (s *TestSuite) SetupTest() { + s.calls = 0 +} +func (s *TestSuite) TestBaselineRoute() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v0/this/is/no/endpoint", ctx) + assert.Equal(s.T(), echo.ErrNotFound, ctx.Handler()(ctx)) + assert.Equal(s.T(), 0, s.calls) +} +func (s *TestSuite) TestAccountPendingTransaction() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v1/account/address-param/transactions/pending", ctx) + assert.Equal(s.T(), "/v1/account/:addr/transactions/pending", ctx.Path()) + assert.Equal(s.T(), "address-param", ctx.Param("addr")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestWaitAfterBlock() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v1/status/wait-for-block-after/123456", ctx) + assert.Equal(s.T(), "/v1/status/wait-for-block-after/:round", ctx.Path()) + assert.Equal(s.T(), "123456", ctx.Param("round")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestAccountInformation() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v1/account/ZBBRQD73JH5KZ7XRED6GALJYJUXOMBBP3X2Z2XFA4LATV3MUJKKMKG7SHA", ctx) + assert.Equal(s.T(), "/v1/account/:addr", ctx.Path()) + assert.Equal(s.T(), "ZBBRQD73JH5KZ7XRED6GALJYJUXOMBBP3X2Z2XFA4LATV3MUJKKMKG7SHA", ctx.Param("addr")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestTransactionInformation() { + ctx := s.e.NewContext(nil, nil) + addr := "ZBBRQD73JH5KZ7XRED6GALJYJUXOMBBP3X2Z2XFA4LATV3MUJKKMKG7SHA" + txid := "ASPB5E72OT2UWSOCQGD5OPT3W4KV4LZZDL7L5MBCC3EBAIJCDHAA" + s.e.Router().Find(http.MethodGet, "/v1/account/"+addr+"/transaction/"+txid, ctx) + assert.Equal(s.T(), "/v1/account/:addr/transaction/:txid", ctx.Path()) + assert.Equal(s.T(), addr, ctx.Param("addr")) + assert.Equal(s.T(), txid, ctx.Param("txid")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestAccountTransaction() { + ctx := s.e.NewContext(nil, nil) + addr := "ZBBRQD73JH5KZ7XRED6GALJYJUXOMBBP3X2Z2XFA4LATV3MUJKKMKG7SHA" + s.e.Router().Find(http.MethodGet, "/v1/account/"+addr+"/transactions", ctx) + assert.Equal(s.T(), "/v1/account/:addr/transactions", ctx.Path()) + assert.Equal(s.T(), addr, ctx.Param("addr")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestBlock() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v1/block/123456", ctx) + assert.Equal(s.T(), "/v1/block/:round", ctx.Path()) + assert.Equal(s.T(), "123456", ctx.Param("round")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestPendingTransactionID() { + ctx := s.e.NewContext(nil, nil) + txid := "ASPB5E72OT2UWSOCQGD5OPT3W4KV4LZZDL7L5MBCC3EBAIJCDHAA" + s.e.Router().Find(http.MethodGet, "/v1/transactions/pending/"+txid, ctx) + assert.Equal(s.T(), "/v1/transactions/pending/:txid", ctx.Path()) + assert.Equal(s.T(), txid, ctx.Param("txid")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestPendingTransactionInformationByAddress() { + ctx := s.e.NewContext(nil, nil) + addr := "ZBBRQD73JH5KZ7XRED6GALJYJUXOMBBP3X2Z2XFA4LATV3MUJKKMKG7SHA" + s.e.Router().Find(http.MethodGet, "/v1/account/"+addr+"/transactions/pending", ctx) + assert.Equal(s.T(), "/v1/account/:addr/transactions/pending", ctx.Path()) + assert.Equal(s.T(), addr, ctx.Param("addr")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestGetAsset() { + ctx := s.e.NewContext(nil, nil) + s.e.Router().Find(http.MethodGet, "/v1/asset/123456", ctx) + assert.Equal(s.T(), "/v1/asset/:index", ctx.Path()) + assert.Equal(s.T(), "123456", ctx.Param("index")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func (s *TestSuite) TestGetTransactionByID() { + ctx := s.e.NewContext(nil, nil) + txid := "ASPB5E72OT2UWSOCQGD5OPT3W4KV4LZZDL7L5MBCC3EBAIJCDHAA" + s.e.Router().Find(http.MethodGet, "/v1/transaction/"+txid, ctx) + assert.Equal(s.T(), "/v1/transaction/:txid", ctx.Path()) + assert.Equal(s.T(), txid, ctx.Param("txid")) + + // Ensure that a handler in the route array was called by checking that the 'calls' variable is incremented. + callsBefore := s.calls + assert.Equal(s.T(), nil, ctx.Handler()(ctx)) + assert.Equal(s.T(), callsBefore+1, s.calls) +} +func TestTestSuite(t *testing.T) { + suite.Run(t, new(TestSuite)) +} diff --git a/daemon/algod/api/server/v1/routes/routes.go b/daemon/algod/api/server/v1/routes/routes.go index c1a553314c..686ae54ba7 100644 --- a/daemon/algod/api/server/v1/routes/routes.go +++ b/daemon/algod/api/server/v1/routes/routes.go @@ -113,7 +113,7 @@ var V1Routes = lib.Routes{ lib.Route{ Name: "pending-transaction-information-by-address", Method: "GET", - Path: "/account/{addr}/transactions/pending", + Path: "/account/:addr/transactions/pending", HandlerFunc: handlers.GetPendingTransactionsByAddress, }, From d868c9116f85a8af38fa33614c78888b1a89eec3 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 24 Jun 2020 16:31:24 -0400 Subject: [PATCH 073/267] Expect test for fast catchup (#1186) When switching to catchup mode, the SetCatchpointCatchupMode function is being called. The function would get blocked by the node mutex, which might be held by Stop. That could lead to a deadlock. To resolve the above, we return a channel from SetCatchpointCatchupMode instead. The operation is carried by a goroutine, which at the ned of operation write the new node context to the channel. This allows the caller to select between the reception of the new context and the expiration of the service. Second issue which is addressed in this PR is the timeout context handling. The existing context cancelation was comparing the returned propagated error with the context error err == cs.ctx.Err(). This would have worked correctly if the context errors would have been retained across all the handlers. However, the http client seems to wrap the underlying context error with it's own when aborting due to context expiration/cancelation. This means that in order to correctly detect context expiration, we need to explicitly test cs.ctx.Err() != nil. --- catchup/catchpointService.go | 39 +++- cmd/goal/network.go | 10 +- node/node.go | 120 +++++++----- scripts/release/test/deb/testDebian.exp | 5 +- test/e2e-go/cli/goal/expect/basicGoalTest.exp | 3 + .../cli/goal/expect/catchpointCatchupTest.exp | 168 +++++++++++++++++ .../catchpointCatchupWebProxy/webproxy.go | 85 +++++++++ test/e2e-go/cli/goal/expect/corsTest.exp | 3 + .../cli/goal/expect/createWalletTest.exp | 3 + .../cli/goal/expect/doubleSpendingTest.exp | 4 + .../cli/goal/expect/goalExpectCommon.exp | 178 ++++++++++++++---- .../cli/goal/expect/goalNodeStatusTest.exp | 3 + test/e2e-go/cli/goal/expect/goalNodeTest.exp | 3 + .../cli/goal/expect/goalTxValidityTest.exp | 3 + test/e2e-go/cli/goal/expect/ledgerTest.exp | 3 + .../e2e-go/cli/goal/expect/limitOrderTest.exp | 3 + .../listExpiredParticipationKeyTest.exp | 8 +- .../expect/multisigCreationDeletionTest.exp | 3 + test/e2e-go/cli/goal/expect/reportTest.exp | 3 + .../features/catchup/basicCatchup_test.go | 2 +- .../catchup/catchpointCatchup_test.go | 115 +---------- test/framework/fixtures/webProxyFixture.go | 139 ++++++++++++++ .../consensus/catchpointtestingprotocol.json | 83 ++++++++ 23 files changed, 774 insertions(+), 212 deletions(-) create mode 100755 test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp create mode 100644 test/e2e-go/cli/goal/expect/catchpointCatchupWebProxy/webproxy.go create mode 100644 test/framework/fixtures/webProxyFixture.go create mode 100644 test/testdata/consensus/catchpointtestingprotocol.json diff --git a/catchup/catchpointService.go b/catchup/catchpointService.go index 5c8313a93d..dd6472939d 100644 --- a/catchup/catchpointService.go +++ b/catchup/catchpointService.go @@ -35,7 +35,7 @@ import ( // CatchpointCatchupNodeServices defines the extenal node support needed // for the catchpoint service to switch the node between "regular" operational mode and catchup mode. type CatchpointCatchupNodeServices interface { - SetCatchpointCatchupMode(bool) (newCtx context.Context) + SetCatchpointCatchupMode(bool) (newContextCh <-chan context.Context) } // CatchpointCatchupStats is used for querying and reporting the current state of the catchpoint catchup process @@ -270,7 +270,7 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) { err = cs.ledgerAccessor.ResetStagingBalances(cs.ctx, true) if err != nil { - if err == cs.ctx.Err() { + if cs.ctx.Err() != nil { return cs.stopOrAbort() } return cs.abort(fmt.Errorf("processStageLedgerDownload failed to reset staging balances : %v", err)) @@ -279,7 +279,10 @@ func (cs *CatchpointCatchupService) processStageLedgerDownload() (err error) { if err == nil { break } - if err == cs.ctx.Err() { + // instead of testing for err == cs.ctx.Err() , we'll check on the context itself. + // this is more robust, as the http client library sometimes wrap the context canceled + // error with other erros. + if cs.ctx.Err() != nil { return cs.stopOrAbort() } @@ -319,7 +322,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro fetcher := fetcherFactory.New() blk, _, client, err = fetcher.FetchBlock(cs.ctx, blockRound) if err != nil { - if err == cs.ctx.Err() { + if cs.ctx.Err() != nil { return cs.stopOrAbort() } if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { @@ -360,7 +363,7 @@ func (cs *CatchpointCatchupService) processStageLastestBlockDownload() (err erro // verify that the catchpoint is valid. err = cs.ledgerAccessor.VerifyCatchpoint(cs.ctx, blk) if err != nil { - if err == cs.ctx.Err() { + if cs.ctx.Err() != nil { return cs.stopOrAbort() } if attemptsCount <= cs.config.CatchupBlockDownloadRetryAttempts { @@ -458,7 +461,7 @@ func (cs *CatchpointCatchupService) processStageBlocksDownload() (err error) { fetcher := fetcherFactory.New() blk, _, client, err = fetcher.FetchBlock(cs.ctx, topBlock.Round()-basics.Round(blocksFetched)) if err != nil { - if err == cs.ctx.Err() { + if cs.ctx.Err() != nil { return cs.stopOrAbort() } if attemptsCount <= uint64(cs.config.CatchupBlockDownloadRetryAttempts) { @@ -584,9 +587,29 @@ func (cs *CatchpointCatchupService) updateStage(newStage ledger.CatchpointCatchu return nil } +// updateNodeCatchupMode requests the node to change it's operational mode from +// catchup mode to normal mode and vice versa. func (cs *CatchpointCatchupService) updateNodeCatchupMode(catchupModeEnabled bool) { - newCtx := cs.node.SetCatchpointCatchupMode(catchupModeEnabled) - cs.ctx, cs.cancelCtxFunc = context.WithCancel(newCtx) + newCtxCh := cs.node.SetCatchpointCatchupMode(catchupModeEnabled) + select { + case newCtx, open := <-newCtxCh: + if open { + cs.ctx, cs.cancelCtxFunc = context.WithCancel(newCtx) + } else { + // channel is closed, this means that the node is stopping + } + case <-cs.ctx.Done(): + // the node context was canceled before the SetCatchpointCatchupMode goroutine had + // the chance of completing. We At this point, the service is shutting down. However, + // we don't know how long it would take for the node mutex until it's become available. + // given that the SetCatchpointCatchupMode gave us a non-buffered channel, it might get blocked + // if we won't be draining that channel. To resolve that, we will create another goroutine here + // which would drain that channel. + go func() { + // We'll wait here for the above goroutine to complete : + <-newCtxCh + }() + } } func (cs *CatchpointCatchupService) updateLedgerFetcherProgress(fetcherStats *ledger.CatchpointCatchupAccessorProgress) { diff --git a/cmd/goal/network.go b/cmd/goal/network.go index 3b34a23265..e354d0bb51 100644 --- a/cmd/goal/network.go +++ b/cmd/goal/network.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/netdeploy" "github.com/algorand/go-algorand/util" ) @@ -93,7 +94,14 @@ var networkCreateCmd = &cobra.Command{ panic(err) } - network, err := netdeploy.CreateNetworkFromTemplate(networkName, networkRootDir, networkTemplateFile, binDir, !noImportKeys, nil, nil) + dataDir := maybeSingleDataDir() + var consensus config.ConsensusProtocols + if dataDir != "" { + // try to load the consensus from there. If there is none, we can just use the built in one. + consensus, _ = config.PreloadConfigurableConsensusProtocols(dataDir) + } + + network, err := netdeploy.CreateNetworkFromTemplate(networkName, networkRootDir, networkTemplateFile, binDir, !noImportKeys, nil, consensus) if err != nil { if noClean { reportInfof(" ** failed ** - Preserving network rootdir '%s'", networkRootDir) diff --git a/node/node.go b/node/node.go index c0e73a89f9..8e1ef79ecf 100644 --- a/node/node.go +++ b/node/node.go @@ -857,6 +857,7 @@ func (node *AlgorandFullNode) StartCatchup(catchpoint string) error { return err } node.catchpointCatchupService.Start(node.ctx) + node.log.Infof("starting catching up toward catchpoint %s", catchpoint) return nil } @@ -876,63 +877,84 @@ func (node *AlgorandFullNode) AbortCatchup(catchpoint string) error { return nil } -// SetCatchpointCatchupMode change the node's operational mode from catchpoint catchup mode and back -func (node *AlgorandFullNode) SetCatchpointCatchupMode(catchpointCatchupMode bool) (newCtx context.Context) { - node.mu.Lock() - if catchpointCatchupMode { - // stop.. - defer func() { +// SetCatchpointCatchupMode change the node's operational mode from catchpoint catchup mode and back, it returns a +// channel which contains the updated node context. This function need to work asyncronisly so that the caller could +// detect and handle the usecase where the node is being shut down while we're switching to/from catchup mode without +// deadlocking on the shared node mutex. +func (node *AlgorandFullNode) SetCatchpointCatchupMode(catchpointCatchupMode bool) (outCtxCh <-chan context.Context) { + // create a non-buffered channel to return the newly created context. The fact that it's non-buffered here + // is imporant, as it allows us to syncronize the "receiving" of the new context before canceling of the previous + // one. + ctxCh := make(chan context.Context) + outCtxCh = ctxCh + go func() { + node.mu.Lock() + // check that the node wasn't canceled. If it have been canceled, it means that the node.Stop() was called, in which case + // we should close the channel. + if node.ctx.Err() == context.Canceled { + close(ctxCh) node.mu.Unlock() - node.waitMonitoringRoutines() - }() - node.net.ClearHandlers() - node.txHandler.Stop() - node.agreementService.Shutdown() - node.catchupService.Stop() - node.txPoolSyncerService.Stop() - node.blockService.Stop() - node.ledgerService.Stop() - node.wsFetcherService.Stop() - - node.cancelCtx() + return + } + if catchpointCatchupMode { + // stop.. + defer func() { + node.mu.Unlock() + node.waitMonitoringRoutines() + }() + node.net.ClearHandlers() + node.txHandler.Stop() + node.agreementService.Shutdown() + node.catchupService.Stop() + node.txPoolSyncerService.Stop() + node.blockService.Stop() + node.ledgerService.Stop() + node.wsFetcherService.Stop() + + prevNodeCancelFunc := node.cancelCtx + + // Set up a context we can use to cancel goroutines on Stop() + node.ctx, node.cancelCtx = context.WithCancel(context.Background()) + ctxCh <- node.ctx + + prevNodeCancelFunc() + return + } + defer node.mu.Unlock() + // start + node.transactionPool.Reset() + node.wsFetcherService.Start() + node.catchupService.Start() + node.agreementService.Start() + node.txPoolSyncerService.Start(node.catchupService.InitialSyncDone) + node.blockService.Start() + node.ledgerService.Start() + node.txHandler.Start() - // Set up a context we can use to cancel goroutines on Stop() - node.ctx, node.cancelCtx = context.WithCancel(context.Background()) - return node.ctx - } - defer node.mu.Unlock() - // start - node.transactionPool.Reset() - node.wsFetcherService.Start() - node.catchupService.Start() - node.agreementService.Start() - node.txPoolSyncerService.Start(node.catchupService.InitialSyncDone) - node.blockService.Start() - node.ledgerService.Start() - node.txHandler.Start() - - // start indexer - if idx, err := node.Indexer(); err == nil { - err := idx.Start() - if err != nil { - node.log.Errorf("indexer failed to start, turning it off - %v", err) - node.config.IsIndexerActive = false + // start indexer + if idx, err := node.Indexer(); err == nil { + err := idx.Start() + if err != nil { + node.log.Errorf("indexer failed to start, turning it off - %v", err) + node.config.IsIndexerActive = false + } else { + node.log.Info("Indexer was started successfully") + } } else { - node.log.Info("Indexer was started successfully") + node.log.Infof("Indexer is not available - %v", err) } - } else { - node.log.Infof("Indexer is not available - %v", err) - } - // Set up a context we can use to cancel goroutines on Stop() - node.ctx, node.cancelCtx = context.WithCancel(context.Background()) + // Set up a context we can use to cancel goroutines on Stop() + node.ctx, node.cancelCtx = context.WithCancel(context.Background()) - node.startMonitoringRoutines() + node.startMonitoringRoutines() - // at this point, the catchpoint catchup is done ( either successfully or not.. ) - node.catchpointCatchupService = nil + // at this point, the catchpoint catchup is done ( either successfully or not.. ) + node.catchpointCatchupService = nil - return node.ctx + ctxCh <- node.ctx + }() + return } diff --git a/scripts/release/test/deb/testDebian.exp b/scripts/release/test/deb/testDebian.exp index 663b396b2c..502cf702e0 100644 --- a/scripts/release/test/deb/testDebian.exp +++ b/scripts/release/test/deb/testDebian.exp @@ -29,7 +29,10 @@ if { [catch { source /expectdir/goalExpectCommon.exp - # Create network + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set TEST_ROOT_DIR_LS_OUTPUT [ eval exec ls $TEST_ROOT_DIR ] diff --git a/test/e2e-go/cli/goal/expect/basicGoalTest.exp b/test/e2e-go/cli/goal/expect/basicGoalTest.exp index 01a60cfa7b..4747690d0d 100755 --- a/test/e2e-go/cli/goal/expect/basicGoalTest.exp +++ b/test/e2e-go/cli/goal/expect/basicGoalTest.exp @@ -18,6 +18,9 @@ if { [catch { set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50Each.json" # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] diff --git a/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp new file mode 100755 index 0000000000..a5e6c03534 --- /dev/null +++ b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp @@ -0,0 +1,168 @@ +#!/usr/bin/expect -f +set err 0 +log_user 1 + +# catchpointCatchupTest - test overview +# +# The goal of the test is to demonstrate the catchpoint catchup functionality using the goal command line interface. +# It does that by deploying a single relay, which advances until it generates a catchpoint. +# Once it does, another node is started, and instructred to catchup using the catchpoint from the first relay. +# To make sure that the second node won't be using the "regular" catchup, we tunnel all the communication between the two using a proxy. +# The proxy is responsible to filter out block number 2. This would prevent the "regular" catchup from working, +# and would be a good test ground for the catchpoint catchup. +# +# The second test is a variation of the first one, but we also stop the node that is catching up in the middle of it's catchup process. +# That allows us to verify that the node persist it's catchpoint catchup mode correctly. +# + +proc spawnCatchpointCatchupWebProxy { TARGET_ENDPOINT RUNTIME REQUEST_DELAY } { + upvar WP_SPAWN_ID WP_SPAWN_ID + set WEBPROXY_LISTEN_ADDRESS "" + + # the timeout is set here to take a long while since the command below would end up compiling the go source code before running it. + # on slow machines, this could take a long while. + set timeout 60 + spawn go run ./catchpointCatchupWebProxy -targetEndpoint "$TARGET_ENDPOINT" -runtime $RUNTIME -requestDelay $REQUEST_DELAY + set WP_SPAWN_ID $spawn_id + expect { + -re {(^[0-9\.]+:[0-9]+)} { set WEBPROXY_LISTEN_ADDRESS $expect_out(1,string) } + timeout {::AlgorandGoal::Abort "timed out waiting for web proxy listen address"} + eof { ::AlgorandGoal::CheckEOF "web proxy failed to start"} + } + + puts "Web proxy listening address is $WEBPROXY_LISTEN_ADDRESS" + return $WEBPROXY_LISTEN_ADDRESS +} + +if { [catch { + source goalExpectCommon.exp + set TEST_ALGO_DIR [lindex $argv 0] + set TEST_DATA_DIR [lindex $argv 1] + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/CatchpointCatchupTestNetwork.json" + + # copy the consensus protocol to this data directory. + exec mkdir -p $TEST_ALGO_DIR + exec cp $TEST_DATA_DIR/consensus/catchpointtestingprotocol.json $TEST_ALGO_DIR/consensus.json + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Update the Primary Node configuration + exec -- cat "$TEST_ROOT_DIR/Primary/config.json" | jq {. |= . + {"CatchpointInterval":4,"EnableRequestLogger":true}} > $TEST_ROOT_DIR/Primary/config.json.new + exec rm $TEST_ROOT_DIR/Primary/config.json + exec mv $TEST_ROOT_DIR/Primary/config.json.new $TEST_ROOT_DIR/Primary/config.json + + set ::GLOBAL_TEST_ALGO_DIR $TEST_ALGO_DIR + set ::GLOBAL_TEST_ROOT_DIR $TEST_ROOT_DIR + set ::GLOBAL_NETWORK_NAME $NETWORK_NAME + + set ::env(ALGOSMALLLAMBDAMSEC) 500 + + # Start the Primary Node + ::AlgorandGoal::StartNode $TEST_ROOT_DIR/Primary + + + # Wait until the primary node reaches round 37. At that point, the catchpoint for round 36 is already done. + ::AlgorandGoal::WaitForRound 37 $TEST_ROOT_DIR/Primary + + # Get primary node listening address: + set PRIMARY_LISTEN_ADDRESS "" + spawn cat $TEST_ROOT_DIR/Primary/algod-listen.net + expect { + -re {http:\/\/([0-9\.]+:[0-9]+)} { set PRIMARY_LISTEN_ADDRESS $expect_out(1,string); exp_continue;} + timeout {::AlgorandGoal::Abort "timed out listing $TEST_ROOT_DIR/Primary/algod-listen.net"} + eof { ::AlgorandGoal::CheckEOF "Unable to list $TEST_ROOT_DIR/Primary/algod-listen.net" } + } + + if { $PRIMARY_LISTEN_ADDRESS == "" } { + ::AlgorandGoal::StopNode $TEST_ROOT_DIR/Primary + puts "Primary node listening address could not be retrieved." + exit 1 + } + + puts "Primary node listening address is $PRIMARY_LISTEN_ADDRESS" + + # start the web proxy + set WP_SPAWN_ID 0 + set WEBPROXY_LISTEN_ADDRESS [spawnCatchpointCatchupWebProxy $PRIMARY_LISTEN_ADDRESS 30 20] + + ::AlgorandGoal::StartNode $TEST_ROOT_DIR/Node False $WEBPROXY_LISTEN_ADDRESS + + ::AlgorandGoal::WaitForRound 1 $TEST_ROOT_DIR/Node + + set CATCHPOINT [::AlgorandGoal::GetNodeLastCatchpoint $TEST_ROOT_DIR/Primary] + + puts "Catchpoint is $CATCHPOINT" + + ::AlgorandGoal::StartCatchup $TEST_ROOT_DIR/Node $CATCHPOINT + + ::AlgorandGoal::WaitForRound 37 $TEST_ROOT_DIR/Node + + ::AlgorandGoal::StopNode $TEST_ROOT_DIR/Node + + # close the web proxy + close -i $WP_SPAWN_ID + + puts "catchpointCatchupTest basic test completed" + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in catchpointCatchupTest - basic test: $EXCEPTION" +} + + # basic testing is done. Now, let's try to see if we can stop and resume a node. +if { [catch { + # delete the genesis directory, so that the node would be on round 0 again. + file delete -force $TEST_ROOT_DIR/Node/node.log + foreach path [glob $TEST_ROOT_DIR/Node/test_net*] { + file delete -force -- $path + } + + # start the proxy. this time, make it go really slow + set WEBPROXY_LISTEN_ADDRESS [spawnCatchpointCatchupWebProxy $PRIMARY_LISTEN_ADDRESS 30 1500] + + ::AlgorandGoal::StartNode $TEST_ROOT_DIR/Node False $WEBPROXY_LISTEN_ADDRESS + + set NODE_ROUND [::AlgorandGoal::WaitForRound 1 $TEST_ROOT_DIR/Node] + + if { $NODE_ROUND >= 35 } { + # it means that we haven't reset the directory correctly. + ::AlgorandGoal::Abort "Node $TEST_ROOT_DIR/Node should have been reset, but it didn't" + } + + ::AlgorandGoal::StartCatchup $TEST_ROOT_DIR/Node $CATCHPOINT + + # wait for the node to start catching up for up to 35 seconds. ( it won't take that long, but it's not immediately either ). + ::AlgorandGoal::WaitCatchup $TEST_ROOT_DIR/Node 35 + + ::AlgorandGoal::StopNode $TEST_ROOT_DIR/Node + + # close the web proxy + close -i $WP_SPAWN_ID + + # restart the web proxy, without the delaying part. + set WEBPROXY_LISTEN_ADDRESS [spawnCatchpointCatchupWebProxy $PRIMARY_LISTEN_ADDRESS 30 20] + + ::AlgorandGoal::StartNode $TEST_ROOT_DIR/Node False $WEBPROXY_LISTEN_ADDRESS + + ::AlgorandGoal::WaitForRound 38 $TEST_ROOT_DIR/Node + + ::AlgorandGoal::StopNode $TEST_ROOT_DIR/Node + + # close the web proxy + close -i $WP_SPAWN_ID + + ::AlgorandGoal::StopNode $TEST_ROOT_DIR/Primary + + puts "catchpointCatchupTest stop/start test completed" +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in catchpointCatchupTest - stop/start: $EXCEPTION" +} diff --git a/test/e2e-go/cli/goal/expect/catchpointCatchupWebProxy/webproxy.go b/test/e2e-go/cli/goal/expect/catchpointCatchupWebProxy/webproxy.go new file mode 100644 index 0000000000..025e3892eb --- /dev/null +++ b/test/e2e-go/cli/goal/expect/catchpointCatchupWebProxy/webproxy.go @@ -0,0 +1,85 @@ +// 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 . + +package main + +import ( + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/test/framework/fixtures" +) + +var webProxyDestination = flag.String("targetEndpoint", "", "target endpoint") +var webProxyRuntime = flag.Int64("runtime", 60, "how many seconds we need to run") +var webProxyRequestDelay = flag.Int64("requestDelay", 0, "how many milliseconds we're going to delay before forwarding the request") +var webProxyLogFile = flag.String("log", "webProxy.log", "optional name of log file") + +func printHelp() { + fmt.Printf("catchpoint catchup web proxy testing utility\n") + fmt.Printf("command line arguments:\n") + flag.PrintDefaults() + +} +func main() { + flag.Parse() + if *webProxyDestination == "" { + printHelp() + return + } + var mu deadlock.Mutex + wp, err := fixtures.MakeWebProxy(*webProxyDestination, func(response http.ResponseWriter, request *http.Request, next http.HandlerFunc) { + mu.Lock() + time.Sleep(time.Duration(*webProxyRequestDelay) * time.Millisecond) + mu.Unlock() + // prevent requests for block #2 to go through. + if strings.HasSuffix(request.URL.String(), "/block/2") { + response.WriteHeader(http.StatusBadRequest) + return + } + if *webProxyLogFile != "" { + f, _ := os.OpenFile(*webProxyLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + f.Write([]byte(fmt.Sprintf("proxy saw request for %s\n", request.URL.String()))) + f.Close() + } + + next(response, request) + }) + if err != nil { + return + } + defer wp.Close() + fmt.Printf("%s\n", wp.GetListenAddress()) + + // Handle signals cleanly + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + signal.Ignore(syscall.SIGHUP) + + select { + case sig := <-c: + fmt.Printf("Exiting webproxy on %v\n", sig) + case <-time.After(time.Duration(*webProxyRuntime) * time.Second): + } +} diff --git a/test/e2e-go/cli/goal/expect/corsTest.exp b/test/e2e-go/cli/goal/expect/corsTest.exp index ca0b431c19..8d18bbce4a 100755 --- a/test/e2e-go/cli/goal/expect/corsTest.exp +++ b/test/e2e-go/cli/goal/expect/corsTest.exp @@ -18,6 +18,9 @@ if { [catch { set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50Each.json" # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR # Set Primary Wallet Name diff --git a/test/e2e-go/cli/goal/expect/createWalletTest.exp b/test/e2e-go/cli/goal/expect/createWalletTest.exp index 117da37cda..d65acb939e 100755 --- a/test/e2e-go/cli/goal/expect/createWalletTest.exp +++ b/test/e2e-go/cli/goal/expect/createWalletTest.exp @@ -23,6 +23,9 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR # Set Primary Wallet Name diff --git a/test/e2e-go/cli/goal/expect/doubleSpendingTest.exp b/test/e2e-go/cli/goal/expect/doubleSpendingTest.exp index 744d249ed4..04991bf1e5 100755 --- a/test/e2e-go/cli/goal/expect/doubleSpendingTest.exp +++ b/test/e2e-go/cli/goal/expect/doubleSpendingTest.exp @@ -23,6 +23,10 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + # Create Network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR # Create a new wallet diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index 46b0322a5f..ed98c2a260 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -57,23 +57,27 @@ proc ::AlgorandGoal::Abort { ERROR } { } # Start the node -proc ::AlgorandGoal::StartNode { TEST_ALGO_DIR {SYSTEMD_MANAGED ""} } { +proc ::AlgorandGoal::StartNode { TEST_ALGO_DIR {SYSTEMD_MANAGED "False"} {PEER_ADDRESS ""} } { set ::GLOBAL_TEST_ALGO_DIR $TEST_ALGO_DIR set timeout 15 - + set GOAL_PARAMS "node start -d $TEST_ALGO_DIR" + if { $PEER_ADDRESS != "" } { + set GOAL_PARAMS "$GOAL_PARAMS -p $PEER_ADDRESS" + } if { [catch { puts "node start with $TEST_ALGO_DIR" - if { $SYSTEMD_MANAGED eq "" } { - spawn goal node start -d $TEST_ALGO_DIR + spawn goal {*}$GOAL_PARAMS + if { $SYSTEMD_MANAGED eq "True" } { expect { timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node start" } - "^Algorand node successfully started!*" {puts "Node started successfully"; close} + "^This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} + eof { ::AlgorandGoal::CheckEOF "Unable to start network" } } } else { - spawn goal node start -d $TEST_ALGO_DIR expect { timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node start" } - "^This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} + "^Algorand node successfully started!*" {puts "Node started successfully"; close} + eof { ::AlgorandGoal::CheckEOF "Unable to start network" } } } } EXCEPTION] } { @@ -94,12 +98,14 @@ proc ::AlgorandGoal::StopNode { TEST_ALGO_DIR {SYSTEMD_MANAGED ""} } { expect { timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node stop" } "*The node was successfully stopped.*" {puts "Node stopped successfully"; close} + eof { close; ::AlgorandGoal::Abort "Did not recieve appropriate message before goal command completion" } } } else { spawn goal node stop -d $TEST_ALGO_DIR expect { timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node stop" } "*This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} + eof { close; ::AlgorandGoal::Abort "Did not recieve appropriate message before goal command completion" } } } } EXCEPTION] } { @@ -134,12 +140,8 @@ proc ::AlgorandGoal::RestartNode { TEST_ALGO_DIR {SYSTEMD_MANAGED ""} } { } } -# Start the network -proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR TEST_ROOT_DIR } { - set ::GLOBAL_TEST_ALGO_DIR $TEST_ALGO_DIR - set ::GLOBAL_TEST_ROOT_DIR $TEST_ROOT_DIR - set ::GLOBAL_NETWORK_NAME $NETWORK_NAME - +# Create the network +proc ::AlgorandGoal::CreateNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR TEST_ROOT_DIR } { # Running on ARM64, it seems that network creation is pretty slow. # 30 second won't be enough here, so I'm changing this to 120 seconds. set timeout 120 @@ -149,14 +151,26 @@ proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR puts "network create $NETWORK_NAME" spawn goal network create --network $NETWORK_NAME --template $NETWORK_TEMPLATE --datadir $TEST_ALGO_DIR --rootdir $TEST_ROOT_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Timed out creating network with timeout: $timeout" } - "^Network $NETWORK_NAME created under.*" { puts "Network $NETWORK_NAME created" ; close } - close + timeout { close; ::AlgorandGoal::Abort "Timed out creating network" } + "^Network $NETWORK_NAME created under.*" { puts "Network $NETWORK_NAME created" ; exp_continue } + eof { ::AlgorandGoal::CheckEOF "Unable to create network" } } + } EXCEPTION ] } { + puts "ERROR in CreateNetwork: $EXCEPTION" + exit 1 + } +} + +# Start the network +proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR TEST_ROOT_DIR } { + set ::GLOBAL_TEST_ALGO_DIR $TEST_ALGO_DIR + set ::GLOBAL_TEST_ROOT_DIR $TEST_ROOT_DIR + set ::GLOBAL_NETWORK_NAME $NETWORK_NAME + if { [catch { # Start network puts "network start $NETWORK_NAME" - spawn goal network start -d $TEST_ALGO_DIR -r $TEST_ROOT_DIR + spawn goal network start -r $TEST_ROOT_DIR expect { timeout { close; ::AlgorandGoal::Abort "Timed out starting network" } ".*Network started under.* { puts "Network $NETWORK_NAME started" ;close } @@ -170,7 +184,7 @@ proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR if { [catch { # Check network status puts "network status $NETWORK_NAME" - spawn goal network status -d $TEST_ALGO_DIR -r $TEST_ROOT_DIR + spawn goal network status -r $TEST_ROOT_DIR expect { timeout { close; ::AlgorandGoal::Abort "Timed out retrieving network status" } ".*Error getting status.*" { close; ::AlgorandGoal::Abort "error getting network status: $expect_out(buffer)""} @@ -698,22 +712,88 @@ proc ::AlgorandGoal::DeleteMultisigAccount { MULTISIG_ADDRESS TEST_PRIMARY_NODE_ } # Wait for node to reach a specific round -proc ::AlgorandGoal::WaitForRound { WAIT_FOR_ROUND_NUMBER TEST_PRIMARY_NODE_DIR } { +proc ::AlgorandGoal::GetNodeLastCatchpoint { NODE_DATA_DIR } { + set CATCHPOINT "" + if { [catch { + # Check node status + puts "spawn node status" + spawn goal node status -d $NODE_DATA_DIR + expect { + timeout { ::AlgorandGoal::Abort "goal node status timed out" } + -re {Last Catchpoint: ([0-9]*#[A-Z2-7]*)} {regexp -- {[0-9]*#[A-Z2-7]*} $expect_out(0,string) CATCHPOINT; exp_continue } + eof { catch wait result; if { [lindex $result 3] != 0 } { ::AlgorandGoal::Abort "failed to perform goal node status : error code [lindex $result 3]"} } + } + if { $CATCHPOINT == "" } { + ::AlgorandGoal::Abort "Last Catchpoint entry was missing from goal node status" + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in GetNodeLastCatchpoint: $EXCEPTION" + } + return $CATCHPOINT +} + +# Start catching up to a specific catchpoint +proc ::AlgorandGoal::StartCatchup { NODE_DATA_DIR CATCHPOINT } { + if { [catch { + # start catchup + puts "spawn node catchup $CATCHPOINT" + spawn goal node catchup $CATCHPOINT -d $NODE_DATA_DIR + expect { + timeout { ::AlgorandGoal::Abort "goal node catchup timed out" } + eof { catch wait result; if { [lindex $result 3] != 0 } { ::AlgorandGoal::Abort "failed to start catching up : error code [lindex $result 3]"} } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in StartCatchup: $EXCEPTION" + } + return $CATCHPOINT +} - puts "node status waiting for Round $WAIT_FOR_ROUND_NUMBER " +# Wait for node to get into catchup mode +proc ::AlgorandGoal::WaitCatchup { TEST_PRIMARY_NODE_DIR WAIT_DURATION_SEC } { + if { [catch { + set i 0 + while { $i < $WAIT_DURATION_SEC } { + # Check node status + puts "spawn node status " + spawn goal node status -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { ::AlgorandGoal::Abort "goal node status timed out" } + -re {Catchpoint: ([0-9]*#[A-Z2-7]*)} { set CATCHPOINT $expect_out(1,string); exp_continue } + eof { catch wait result; if { [lindex $result 3] != 0 } { ::AlgorandGoal::Abort "failed to wait for catchup mode : error code [lindex $result 3]"} } + } + if { [info exists CATCHPOINT] } { + break + } + incr i + exec sleep 1 + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in WaitCatchup: $EXCEPTION" + } + if { [info exists CATCHPOINT] == 0 } { + ::AlgorandGoal::Abort "Node failed to start catching up within $WAIT_DURATION_SEC seconds" + } +} + +# Wait for node to reach a specific round +proc ::AlgorandGoal::WaitForRound { WAIT_FOR_ROUND_NUMBER NODE_DATA_DIR } { + puts "node status waiting for Round $WAIT_FOR_ROUND_NUMBER " + set LAST_ROUND -1 + set SLEEP_TIME 1 if { [catch { set i 0 while 1 { incr i - exec sleep 10 # Check node status - puts "spawn node status " - spawn goal node status -d $TEST_PRIMARY_NODE_DIR + puts "spawn node status" + log_user 0 + set BLOCK -1 + spawn goal node status -d $NODE_DATA_DIR expect { timeout { ::AlgorandGoal::Abort "goal node status timed out" } - -re {Cannot contact Algorand node: (\d+)} {set BLOCK 0; close } + -re {Cannot contact Algorand node: (\d+)} {set BLOCK -1; close } -re {Last committed block: (\d+)} {set BLOCK $expect_out(1,string); exp_continue } -re {Time since last block: ([0-9]*\.?[0-9]*)s} {set TIME_SINCE_LAST_BLOCK $expect_out(1,string); exp_continue } -re {Sync Time: ([0-9]*\.?[0-9]*)s} {set SYNC_TIME $expect_out(1,string); exp_continue } @@ -722,19 +802,15 @@ proc ::AlgorandGoal::WaitForRound { WAIT_FOR_ROUND_NUMBER TEST_PRIMARY_NODE_DIR -re {Round for next consensus protocol: (\d+)} {set ROUND_FOR_NEXT_CONSENSUS_PROTOCOL $expect_out(1,string); exp_continue } -re {Next consensus protocol supported: (\w+)} {set NEXT_CONSENSUS_PROTOCOL_SUPPORTED $expect_out(1,string); exp_continue } -re {Genesis ID: (\w+)} {set GENESIS_ID $expect_out(1,string); exp_continue } - -re {Genesis hash: ([A-Za-z0-9+/]+={0,2})} {set GENESIS_HASH $expect_out(1,string); close } + -re {Genesis hash: ([A-Za-z0-9+/]+={0,2})} {set GENESIS_HASH $expect_out(1,string); exp_continue } + -re {Catchpoint: ([0-9]*#[A-Z2-7]*)} { set CATCHPOINT $expect_out(1,string); exp_continue } + eof { catch wait result; if { [lindex $result 3] != 0 } { ::AlgorandGoal::Abort "failed to wait for round : error code [lindex $result 3]"} } } - if { $BLOCK > 0 } { - puts "node status check complete" - puts "block: $BLOCK" - puts "time since last block: $TIME_SINCE_LAST_BLOCK" - puts "sync time: $SYNC_TIME" - puts "last consensus protocol: $LAST_CONSENSUS_PROTOCOL" - puts "next consensus protocol: $NEXT_CONSENSUS_PROTOCOL" - puts "round for next consensus protocol: $ROUND_FOR_NEXT_CONSENSUS_PROTOCOL" - puts "next consensus protocol supported: $NEXT_CONSENSUS_PROTOCOL_SUPPORTED" - puts "genesis id: $GENESIS_ID" - puts "genesis hash: $GENESIS_HASH" + log_user 1 + if { $BLOCK > -1 } { + puts "node status check complete, current round is $BLOCK" + } else { + ::AlgorandGoal::Abort "failed to retrieve block round number" } # Check if the round number is reached @@ -742,8 +818,20 @@ proc ::AlgorandGoal::WaitForRound { WAIT_FOR_ROUND_NUMBER TEST_PRIMARY_NODE_DIR puts "Reached Round number: $WAIT_FOR_ROUND_NUMBER"; break } else { puts "Current Round: '$BLOCK' is less than wait for round: '$WAIT_FOR_ROUND_NUMBER'" + if { $LAST_ROUND >= $BLOCK } { + # no progress was made since last time we checked. + incr SLEEP_TIME + } else { + # we've made progress since last time we checked. + if { $SLEEP_TIME > 0 } { incr SLEEP_TIME -1 } + set i 0 + set LAST_ROUND $BLOCK + } if { $i >= 10 } then { ::AlgorandGoal::Abort " Current Round $BLOCK did not reach $WAIT_FOR_ROUND_NUMBER "; break;} } + + puts "sleep time $SLEEP_TIME" + after [expr {int($SLEEP_TIME * 1000)}] } } EXCEPTION ] } { ::AlgorandGoal::Abort "ERROR in WaitForRound: $EXCEPTION" @@ -794,8 +882,11 @@ proc ::AlgorandGoal::AddParticipationKey { ADDRESS FIRST_ROUND LAST_ROUND TEST_P # Register online participation with a given account proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRIMARY_NODE_DIR } { - if { [ catch { - spawn goal account changeonlinestatus --address $ADDRESS --firstvalid $FIRST_ROUND --lastvalid $LAST_ROUND -d datadir + # we need to set the timeout to more than one round, since it migth take few rounds for the transaction to be accepted, transmitted, proposed, etc. + set timeout 20 + + if { [ catch { + spawn goal account changeonlinestatus --address $ADDRESS --firstvalid $FIRST_ROUND --lastvalid $LAST_ROUND -d $TEST_PRIMARY_NODE_DIR expect { timeout { ::AlgorandGoal::Abort "goal TakeAccountOnline timed out" } close @@ -804,3 +895,14 @@ proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRI ::AlgorandGoal::Abort "ERROR in TakeAccountOnline: $EXCEPTION" } } + +# CheckEOF checks if there was an error, and aborts if there was an error +proc ::AlgorandGoal::CheckEOF { { ERROR_STRING "" } } { + upvar spawn_id spawn_id + catch { wait -i $spawn_id } result; + if { [lindex $result 3] != 0 } { + puts $ERROR_STRING + puts "returned error code is [lindex $result 3]" + exit 1 + } +} diff --git a/test/e2e-go/cli/goal/expect/goalNodeStatusTest.exp b/test/e2e-go/cli/goal/expect/goalNodeStatusTest.exp index 554bf56883..9a7ccfdf89 100755 --- a/test/e2e-go/cli/goal/expect/goalNodeStatusTest.exp +++ b/test/e2e-go/cli/goal/expect/goalNodeStatusTest.exp @@ -18,6 +18,9 @@ if { [catch { set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50Each.json" # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR exec sleep 5 diff --git a/test/e2e-go/cli/goal/expect/goalNodeTest.exp b/test/e2e-go/cli/goal/expect/goalNodeTest.exp index d06375f2cc..2a71aa56d9 100644 --- a/test/e2e-go/cli/goal/expect/goalNodeTest.exp +++ b/test/e2e-go/cli/goal/expect/goalNodeTest.exp @@ -20,6 +20,9 @@ if { [catch { set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50Each.json" # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR exec sleep 5 diff --git a/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp b/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp index 6fa4d333d2..4c791d2e3d 100644 --- a/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp +++ b/test/e2e-go/cli/goal/expect/goalTxValidityTest.exp @@ -46,6 +46,9 @@ proc TestLastValidInTx { CMD TX_FILE EXPECTED_LAST_VALID } { if { [catch { # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR # use goal node status command to wait for round 0 diff --git a/test/e2e-go/cli/goal/expect/ledgerTest.exp b/test/e2e-go/cli/goal/expect/ledgerTest.exp index e5e6823081..dd279c1bac 100755 --- a/test/e2e-go/cli/goal/expect/ledgerTest.exp +++ b/test/e2e-go/cli/goal/expect/ledgerTest.exp @@ -22,6 +22,9 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] diff --git a/test/e2e-go/cli/goal/expect/limitOrderTest.exp b/test/e2e-go/cli/goal/expect/limitOrderTest.exp index 7c8275e59c..3bacb002b1 100644 --- a/test/e2e-go/cli/goal/expect/limitOrderTest.exp +++ b/test/e2e-go/cli/goal/expect/limitOrderTest.exp @@ -18,6 +18,9 @@ if { [catch { set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] diff --git a/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp b/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp index 20305e7d1c..8cbcbd6bb0 100644 --- a/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp +++ b/test/e2e-go/cli/goal/expect/listExpiredParticipationKeyTest.exp @@ -23,11 +23,17 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set PRIMARY_WALLET_NAME unencrypted-default-wallet - set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Wait for the network to reach the first round. + ::AlgorandGoal::WaitForRound 1 $TEST_PRIMARY_NODE_DIR # Register participation keys set ROUND_FIRST_VALID 1 diff --git a/test/e2e-go/cli/goal/expect/multisigCreationDeletionTest.exp b/test/e2e-go/cli/goal/expect/multisigCreationDeletionTest.exp index e74d583099..de199864fe 100755 --- a/test/e2e-go/cli/goal/expect/multisigCreationDeletionTest.exp +++ b/test/e2e-go/cli/goal/expect/multisigCreationDeletionTest.exp @@ -23,6 +23,9 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR # Set Wallet Name and verify it diff --git a/test/e2e-go/cli/goal/expect/reportTest.exp b/test/e2e-go/cli/goal/expect/reportTest.exp index d05b296271..f01bf43f8a 100644 --- a/test/e2e-go/cli/goal/expect/reportTest.exp +++ b/test/e2e-go/cli/goal/expect/reportTest.exp @@ -22,6 +22,9 @@ if { [catch { exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] diff --git a/test/e2e-go/features/catchup/basicCatchup_test.go b/test/e2e-go/features/catchup/basicCatchup_test.go index 2a414ce510..12a2d04963 100644 --- a/test/e2e-go/features/catchup/basicCatchup_test.go +++ b/test/e2e-go/features/catchup/basicCatchup_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package rewards +package catchup import ( "os" diff --git a/test/e2e-go/features/catchup/catchpointCatchup_test.go b/test/e2e-go/features/catchup/catchpointCatchup_test.go index 758cb69094..c80518a37b 100644 --- a/test/e2e-go/features/catchup/catchpointCatchup_test.go +++ b/test/e2e-go/features/catchup/catchpointCatchup_test.go @@ -14,17 +14,14 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package rewards +package catchup import ( - "fmt" - "net" "net/http" "os" "os/exec" "path/filepath" "runtime" - "strings" "syscall" "testing" "time" @@ -144,7 +141,7 @@ func TestBasicCatchpointCatchup(t *testing.T) { primaryListeningAddress, err := primaryNode.GetListeningAddress() a.NoError(err) - wp, err := makeWebProxy(primaryListeningAddress, func(response http.ResponseWriter, request *http.Request, next http.HandlerFunc) { + wp, err := fixtures.MakeWebProxy(primaryListeningAddress, func(response http.ResponseWriter, request *http.Request, next http.HandlerFunc) { // prevent requests for block #2 to go through. if request.URL.String() == "/v1/test-v1/block/2" { response.WriteHeader(http.StatusBadRequest) @@ -205,111 +202,3 @@ func TestBasicCatchpointCatchup(t *testing.T) { secondNode.StopAlgod() primaryNode.StopAlgod() } - -type webProxyInterceptFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc) - -type webProxy struct { - server *http.Server - listener net.Listener - destination string - intercept webProxyInterceptFunc -} - -func makeWebProxy(destination string, intercept webProxyInterceptFunc) (wp *webProxy, err error) { - if strings.HasPrefix(destination, "http://") { - destination = destination[7:] - } - wp = &webProxy{ - destination: destination, - intercept: intercept, - } - wp.server = &http.Server{ - Handler: wp, - } - wp.listener, err = net.Listen("tcp", "localhost:") - if err != nil { - return nil, err - } - go func() { - wp.server.Serve(wp.listener) - }() - return wp, nil -} - -func (wp *webProxy) GetListenAddress() string { - return wp.listener.Addr().String() -} - -func (wp *webProxy) Close() { - // we can't use shutdown, since we have tunneled websocket, which is a hijacked connection - // that http.Server doens't know how to handle. - wp.server.Close() -} - -func (wp *webProxy) ServeHTTP(response http.ResponseWriter, request *http.Request) { - //fmt.Printf("incoming request for %v\n", request.URL) - if wp.intercept == nil { - wp.Passthrough(response, request) - return - } - wp.intercept(response, request, wp.Passthrough) -} - -func (wp *webProxy) Passthrough(response http.ResponseWriter, request *http.Request) { - client := http.Client{} - clientRequestURL := *request.URL - clientRequestURL.Scheme = "http" - clientRequestURL.Host = wp.destination - clientRequest, err := http.NewRequest(request.Method, clientRequestURL.String(), request.Body) - if err != nil { - fmt.Printf("Passthrough request assembly error %v (%#v)\n", err, clientRequestURL) - response.WriteHeader(http.StatusInternalServerError) - return - } - if request.Header != nil { - for headerKey, headerValues := range request.Header { - for _, headerValue := range headerValues { - clientRequest.Header.Add(headerKey, headerValue) - } - } - } - clientResponse, err := client.Do(clientRequest) - if err != nil { - fmt.Printf("Passthrough request error %v (%v)\n", err, request.URL.String()) - response.WriteHeader(http.StatusInternalServerError) - return - } - if clientResponse.Header != nil { - for headerKey, headerValues := range clientResponse.Header { - for _, headerValue := range headerValues { - response.Header().Add(headerKey, headerValue) - } - } - } - response.WriteHeader(clientResponse.StatusCode) - ch := make(chan []byte, 10) - go func(outCh chan []byte) { - defer close(outCh) - if clientResponse.Body == nil { - return - } - defer clientResponse.Body.Close() - for { - buf := make([]byte, 4096) - n, err := clientResponse.Body.Read(buf) - if n > 0 { - outCh <- buf[:n] - } - if err != nil { - break - } - - } - }(ch) - for bytes := range ch { - response.Write(bytes) - if flusher, has := response.(http.Flusher); has { - flusher.Flush() - } - } -} diff --git a/test/framework/fixtures/webProxyFixture.go b/test/framework/fixtures/webProxyFixture.go new file mode 100644 index 0000000000..4cba12ee3f --- /dev/null +++ b/test/framework/fixtures/webProxyFixture.go @@ -0,0 +1,139 @@ +// 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 . + +package fixtures + +import ( + "fmt" + "net" + "net/http" + "strings" +) + +// WebProxyInterceptFunc expose the web proxy intercept function +type WebProxyInterceptFunc func(http.ResponseWriter, *http.Request, http.HandlerFunc) + +// WebProxy is the web proxy instance +type WebProxy struct { + server *http.Server + listener net.Listener + destination string + intercept WebProxyInterceptFunc +} + +// MakeWebProxy creates an instance of the web proxy +func MakeWebProxy(destination string, intercept WebProxyInterceptFunc) (wp *WebProxy, err error) { + if strings.HasPrefix(destination, "http://") { + destination = destination[7:] + } + wp = &WebProxy{ + destination: destination, + intercept: intercept, + } + wp.server = &http.Server{ + Handler: wp, + } + wp.listener, err = net.Listen("tcp", "localhost:") + if err != nil { + return nil, err + } + go func() { + wp.server.Serve(wp.listener) + }() + return wp, nil +} + +// GetListenAddress retrieves the listening address of the web proxy +func (wp *WebProxy) GetListenAddress() string { + return wp.listener.Addr().String() +} + +// Close release the web proxy resources +func (wp *WebProxy) Close() { + // we can't use shutdown, since we have tunneled websocket, which is a hijacked connection + // that http.Server doens't know how to handle. + wp.server.Close() +} + +// ServeHTTP serves a single HTTP request +func (wp *WebProxy) ServeHTTP(response http.ResponseWriter, request *http.Request) { + //fmt.Printf("incoming request for %v\n", request.URL) + if wp.intercept == nil { + wp.Passthrough(response, request) + return + } + wp.intercept(response, request, wp.Passthrough) +} + +// Passthrough is the default web proxy implemented function for passing a requests through without modifying it. +func (wp *WebProxy) Passthrough(response http.ResponseWriter, request *http.Request) { + client := http.Client{} + clientRequestURL := *request.URL + clientRequestURL.Scheme = "http" + clientRequestURL.Host = wp.destination + clientRequest, err := http.NewRequest(request.Method, clientRequestURL.String(), request.Body) + if err != nil { + fmt.Printf("Passthrough request assembly error %v (%#v)\n", err, clientRequestURL) + response.WriteHeader(http.StatusInternalServerError) + return + } + if request.Header != nil { + for headerKey, headerValues := range request.Header { + for _, headerValue := range headerValues { + clientRequest.Header.Add(headerKey, headerValue) + } + } + } + clientResponse, err := client.Do(clientRequest) + if err != nil { + fmt.Printf("Passthrough request error %v (%v)\n", err, request.URL.String()) + response.WriteHeader(http.StatusInternalServerError) + return + } + if clientResponse.Header != nil { + for headerKey, headerValues := range clientResponse.Header { + for _, headerValue := range headerValues { + response.Header().Add(headerKey, headerValue) + } + } + } + response.WriteHeader(clientResponse.StatusCode) + ch := make(chan []byte, 10) + go func(outCh chan []byte) { + defer close(outCh) + if clientResponse.Body == nil { + return + } + defer clientResponse.Body.Close() + for { + buf := make([]byte, 4096) + n, err := clientResponse.Body.Read(buf) + if n > 0 { + outCh <- buf[:n] + } + if err != nil { + break + } + + } + }(ch) + for bytes := range ch { + response.Write(bytes) + if flusher, has := response.(http.Flusher); has { + flusher.Flush() + } + } +} diff --git a/test/testdata/consensus/catchpointtestingprotocol.json b/test/testdata/consensus/catchpointtestingprotocol.json new file mode 100644 index 0000000000..f6c587cfc2 --- /dev/null +++ b/test/testdata/consensus/catchpointtestingprotocol.json @@ -0,0 +1,83 @@ +{ + "catchpointtestingprotocol": { + "UpgradeVoteRounds": 10000, + "UpgradeThreshold": 9000, + "DefaultUpgradeWaitRounds": 140000, + "MinUpgradeWaitRounds": 10000, + "MaxUpgradeWaitRounds": 150000, + "MaxVersionStringLen": 128, + "MaxTxnBytesPerBlock": 1000000, + "MaxTxnNoteBytes": 1024, + "MaxTxnLife": 33, + "ApprovedUpgrades": {}, + "SupportGenesisHash": true, + "RequireGenesisHash": true, + "DefaultKeyDilution": 10000, + "MinBalance": 100000, + "MinTxnFee": 1000, + "RewardUnit": 1000000, + "RewardsRateRefreshInterval": 500000, + "SeedLookback": 2, + "SeedRefreshInterval": 8, + "MaxBalLookback": 32, + "NumProposers": 20, + "SoftCommitteeSize": 2990, + "SoftCommitteeThreshold": 2267, + "CertCommitteeSize": 1500, + "CertCommitteeThreshold": 1112, + "NextCommitteeSize": 5000, + "NextCommitteeThreshold": 3838, + "LateCommitteeSize": 500, + "LateCommitteeThreshold": 320, + "RedoCommitteeSize": 2400, + "RedoCommitteeThreshold": 1768, + "DownCommitteeSize": 6000, + "DownCommitteeThreshold": 4560, + "FastRecoveryLambda": 300000000000, + "FastPartitionRecovery": true, + "PaysetCommitFlat": true, + "MaxTimestampIncrement": 25, + "SupportSignedTxnInBlock": true, + "ForceNonParticipatingFeeSink": true, + "ApplyData": true, + "RewardsInApplyData": true, + "CredentialDomainSeparationEnabled": true, + "SupportBecomeNonParticipatingTransactions": true, + "PendingResidueRewards": true, + "Asset": true, + "MaxAssetsPerAccount": 1000, + "MaxAssetNameBytes": 32, + "MaxAssetUnitNameBytes": 8, + "MaxAssetURLBytes": 32, + "TxnCounter": true, + "SupportTxGroups": true, + "MaxTxGroupSize": 16, + "SupportTransactionLeases": true, + "FixTransactionLeases": true, + "LogicSigVersion": 1, + "LogicSigMaxSize": 1000, + "LogicSigMaxCost": 20000, + "MaxAssetDecimals": 19, + "UseBuggyProposalLowestOutput": false, + "SupportRekeying": false, + "Application": false, + "MaxAppArgs": 0, + "MaxAppTotalArgLen": 0, + "MaxAppProgramLen": 0, + "MaxAppTxnAccounts": 0, + "MaxAppTxnForeignApps": 0, + "MaxAppProgramCost": 0, + "MaxAppKeyLen": 0, + "MaxAppBytesValueLen": 0, + "MaxAppsCreated": 0, + "MaxAppsOptedIn": 0, + "AppFlatParamsMinBalance": 0, + "AppFlatOptInMinBalance": 0, + "SchemaMinBalancePerEntry": 0, + "SchemaUintMinBalance": 0, + "SchemaBytesMinBalance": 0, + "MaxLocalSchemaEntries": 0, + "MaxGlobalSchemaEntries": 0, + "MaximumMinimumBalance": 0 + } +} From 179590b8f56291b8327d4dfdabb38b84a6640dc1 Mon Sep 17 00:00:00 2001 From: algonautshant <55754073+algonautshant@users.noreply.github.com> Date: Wed, 24 Jun 2020 17:53:53 -0400 Subject: [PATCH 074/267] Mark the in-body parameter as binary (#1171) Mark the compile teal param as binary in the spec file --- daemon/algod/api/algod.oas2.json | 3 ++- daemon/algod/api/algod.oas3.yml | 1 + .../algod/api/server/v2/generated/routes.go | 26 +++++++++---------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 4198b89e98..b7baa4ce88 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -726,7 +726,8 @@ "in": "body", "required": true, "schema": { - "type": "string" + "type": "string", + "format": "binary" } } ], diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 7567483b4b..beb5cf8429 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -1878,6 +1878,7 @@ "content": { "text/plain": { "schema": { + "format": "binary", "type": "string" } } diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 90cda3f6df..5bf9908564 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -554,19 +554,19 @@ var swaggerSpec = []string{ "qE3of6fMfUCGuthKC2MsgPrPCVlD4+ZxjWCrggx3IOIu6Po2oSzDuvumJ2hAvRByq3xtlQTVgpiFkYJr", "5mtBsIOB9+e+vOTnzlPdeao7T3Xnqe481Z2nuvNUf1+e6icsG2gc30TeUPvqilBtBdkVV/yOiisqB7t0", "r9EhN+6w0e+1hyAaaDp23XLxvFio3krss++PXxIlChkDiQ04xkmeUrxrutS+dJCs6/eLNqjbbth3lFzb", - "c7gbKZwBTV1vZOfFg9JPRbJq8dWgN0ZMrxmedVashf0KKMLshg5Xt1oO8Z/brPnz2U+CGDmhqmzFrsT8", - "Y4yTJ2NQjRincjU0EpYUMRDs62HlZxmZQTPgkVPpaCKSle+5YeepDFirtNQbsKaleEsX9ULVdcaiTtZl", - "ZNFskra6A2YfDrcwJHNAHSoVK1CHa4giBU1i49Fr4du13rGR+TqamH9Gg1BdOTl2lUUNauysw+/FlXrq", - "lU8Rit08W8ppoynUydFGKyXpQi950EqNqxZPwZPwTuvi2z0R33Vy33Vy33Vy33Vy33Vy3+3cv6MbJq2m", - "YSXj8fOKbd737Mu3cKH1y77FuvE4dXdndHdndHdndMs7o1uUxO+4u7sR/BXfCP6d3fn5fd2PuUvX7a5X", - "86XfNR6t9RDHH/SSJZsbF3293ygmt/WJYnJXXyj+zN8nDrjc3d39Ov2jWsISLlg0YnfNdjF/2qZXzH+K", - "e/0cNGWpKusoA9EUxjWhb9iXilv/bL4tvys/pe+/SmKhpOwC6pVNWBK7oDLxI7qum2t4Hv6MwFnVstkM", - "8E5AG9FpCY1V3bfLhubh0rtUKLjhp+ZxDqNDlOE356svMPXDZHwW9TX/f2af+4+++RRY+Ev09Xk9e6KN", - "n1DyLceZ6hCxzuQpcdcEwwBrn7Ff8zml0ui0IV2w+AISYgTSt8Dt8RXJPde9zvWtX8xXvibV2rv7I0KO", - "uW1CTawKtVKaLeD8G70O/rJuoZumL1BQgB+6kjeUIj/NetlRYETshqDsJOsB6SXvESC6CERO23YkCARK", - "rbClJlQWi20ilK/f72i/8/GOR3um2/M8Prvv8TkPxL+MpPldNm9YW6DwWmjyAreVm0UoZUvTkAdikfBd", - "dtFZLPvrvntvXCJsCe/8yKpp7NF4nIqYpnOh9HhgvLxmQ9n6Q2NO6MzO4Py0XLJL7I7y/ur/AgAA//+K", - "EZxO7qcAAA==", + "c7gbKZwBTV1vZOfFg9JPRbJq8dWgN0ZMmxytLnMwTmWgI16gr2CbBlrY74IiFt1g4upWCyT+c9s3fz6L", + "ShAjJ2aV9dgVnX+MufJkDKoRKuHQSFhSxECw04eVn2VkBs2AR07Jo4lIVr4Lh52nMmmtYlNv0pq24y1d", + "1EtX15mPOlmXkbMVNzYkc0AdKhUrUJlriCIFTWLj42vhG7jesZH5Otqaf0aDUF1COXa1Rg1q7KzD78W5", + "euqVTxGK/T1bymnjK9TJ0UYrJelCL3nQSo2rpk/Bs/FOM+PbPSPf9Xbf9Xbf9Xbf9Xbf9Xbf7dy/ozsn", + "rTZiJePxg4tt3vfsy7dwxfXLvte68YB1d4t0d4t0d4t0y1ukWxTJ77i7uyP8Fd8R/p3dAvp93Zi5S9ft", + "rlfzpd8+Hq31EMcf9JIlm1sZfb1fLSa39dFiclffLP7MXywOuNzd3f06HaVawhIuYTRid80GMn/apnvM", + "f4p7/Rw0ZakqKysD0RTGNaGv2peKW/+Qvi3IKz+u779TYqGk7ALqtU5YJLugMvEjuq6ba4Ee/rDAWdXE", + "2QzwTkAb0WkJjVX9uMsW5+FivFQouOHH53EOo0OU4Vfoq28y9cNkfBb1fQ7gmX3uPwPnU2Dhb9PX5/Xs", + "iTZ+VMk3IWeqQ8Q6k6fEXRwMA6x92H7NB5ZKo9OGdMHiC0iIEUjfFLfHVyT3XD8718l+MV/5KlVr7+6P", + "CDnmti01sSrUSmm2gPNv9Dr4y7qFbpq+QEEBfvpK3lCK/DTrZUeBEbEbgrKTrAekl7xHgOgiEDlt26Mg", + "ECi1wpaaUFkstolQvn6/o/3Oxzse7Zluz/P47L7H5zwQ/zKS5nfZzmFtgcJrockL3FZuFqGUTU5DHohF", + "wvfdRWex7Lj77r1xibBJvPMjqzayR+NxKmKazoXS44Hx8potZusPjTmhMzuD89NyyS6xX8r7q/8LAAD/", + "/19yhfQAqAAA", } // GetSwagger returns the Swagger specification corresponding to the generated code From 5a33f0bb4db9bdfe3193ab95d053852b10d81c48 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 25 Jun 2020 09:07:36 -0400 Subject: [PATCH 075/267] initial implementation --- ledger/acctupdates.go | 166 ++++++++++++++++++++++++++----------- ledger/acctupdates_test.go | 2 +- ledger/archival_test.go | 9 +- ledger/eval.go | 4 +- ledger/ledger.go | 22 +++-- ledger/tracker.go | 2 +- 6 files changed, 139 insertions(+), 66 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 0a8aca97f9..9c42841f82 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -35,6 +35,7 @@ import ( "github.com/algorand/go-algorand/crypto/merkletrie" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" @@ -210,6 +211,8 @@ func (au *accountUpdates) initialize(cfg config.Local, dbPathPrefix string, gene // loadFromDisk is the 2nd level initializtion, and is required before the accountUpdates becomes functional // The close function is expected to be call in pair with loadFromDisk func (au *accountUpdates) loadFromDisk(l ledgerForTracker) error { + au.accountsMu.Lock() + defer au.accountsMu.Unlock() var writingCatchpointRound uint64 lastBalancesRound, lastestBlockRound, err := au.initializeFromDisk(l) @@ -221,7 +224,6 @@ func (au *accountUpdates) loadFromDisk(l ledgerForTracker) error { writingCatchpointRound, _, err = au.accountsq.readCatchpointStateUint64(context.Background(), catchpointStateWritingCatchpoint) if err != nil { - au.accountsMu.Unlock() return err } @@ -246,9 +248,13 @@ func (au *accountUpdates) close() { au.waitAccountsWriting() } -func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { +func (au *accountUpdates) Lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() + return au.lookup(rnd, addr, withRewards) +} + +func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { offset, err := au.roundOffset(rnd) if err != nil { return @@ -367,41 +373,11 @@ func (au *accountUpdates) getLastCatchpointLabel() string { return au.lastCatchpointLabel } -// getAssetCreatorForRound returns the asset creator for a given asset index at a given round -func (au *accountUpdates) getAssetCreatorForRound(rnd basics.Round, aidx basics.AssetIndex) (basics.Address, error) { +// GetAssetCreatorForRound returns the asset creator for a given asset index at a given round +func (au *accountUpdates) GetAssetCreatorForRound(rnd basics.Round, aidx basics.AssetIndex) (basics.Address, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - offset, err := au.roundOffset(rnd) - if err != nil { - return basics.Address{}, err - } - - // If this is the most recent round, au.assets has will have the latest - // state and we can skip scanning backwards over assetDeltas - if offset == uint64(len(au.deltas)) { - // Check if we already have the asset/creator in cache - assetDelta, ok := au.assets[aidx] - if ok { - if assetDelta.created { - return assetDelta.creator, nil - } - return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) - } - } else { - for offset > 0 { - offset-- - assetDelta, ok := au.assetDeltas[offset][aidx] - if ok { - if assetDelta.created { - return assetDelta.creator, nil - } - return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) - } - } - } - - // Check the database - return au.accountsq.lookupAssetCreator(aidx) + return au.getAssetCreatorForRound(rnd, aidx) } // committedUpTo enqueues commiting the balances for round committedRound-lookback. @@ -516,7 +492,10 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { au.accountsMu.Lock() defer au.accountsMu.Unlock() + au.newBlockImpl(blk, delta) +} +func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) { proto := config.Consensus[blk.CurrentProtocol] rnd := blk.Round() @@ -568,16 +547,11 @@ func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { au.roundTotals = append(au.roundTotals, newTotals) } -func (au *accountUpdates) totals(rnd basics.Round) (totals AccountTotals, err error) { +func (au *accountUpdates) Totals(rnd basics.Round) (totals AccountTotals, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - offset, err := au.roundOffset(rnd) - if err != nil { - return - } - totals = au.roundTotals[offset] - return + return au.totals(rnd) } func (au *accountUpdates) getCatchpointStream(round basics.Round) (io.ReadCloser, error) { @@ -635,11 +609,108 @@ func (au *accountUpdates) getCatchpointStream(round basics.Round) (io.ReadCloser // functions below this line are all internal functions +type accountUpdatesLedgerEvaluator struct { + au *accountUpdates + prevHeader bookkeeping.BlockHeader +} + +func (aul *accountUpdatesLedgerEvaluator) GenesisHash() crypto.Digest { + return aul.prevHeader.GenesisHash +} + +func (aul *accountUpdatesLedgerEvaluator) BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) { + if r == aul.prevHeader.Round { + return aul.prevHeader, nil + } + return bookkeeping.BlockHeader{}, ErrNoEntry{} +} + +func (aul *accountUpdatesLedgerEvaluator) Lookup(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { + return aul.au.lookup(rnd, addr, true) +} + +func (aul *accountUpdatesLedgerEvaluator) Totals(rnd basics.Round) (AccountTotals, error) { + return aul.au.totals(rnd) +} + +func (aul *accountUpdatesLedgerEvaluator) isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) { + // this is a non-issue since this call will never be made on non-validating evaluation + return false, fmt.Errorf("accountUpdatesLedgerEvaluator: tried to check for dup during accountUpdates initilization ") +} + +func (aul *accountUpdatesLedgerEvaluator) GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) { + // this is a non-issue since this call will never be made on non-validating evaluation + return nil +} + +func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { + return aul.au.lookup(rnd, addr, false) +} + +func (aul *accountUpdatesLedgerEvaluator) GetAssetCreatorForRound(rnd basics.Round, assetIdx basics.AssetIndex) (basics.Address, error) { + return aul.au.getAssetCreatorForRound(rnd, assetIdx) +} + +// getAssetCreatorForRound returns the asset creator for a given asset index at a given round +func (au *accountUpdates) getAssetCreatorForRound(rnd basics.Round, aidx basics.AssetIndex) (basics.Address, error) { + offset, err := au.roundOffset(rnd) + if err != nil { + return basics.Address{}, err + } + + // If this is the most recent round, au.assets has will have the latest + // state and we can skip scanning backwards over assetDeltas + if offset == uint64(len(au.deltas)) { + // Check if we already have the asset/creator in cache + assetDelta, ok := au.assets[aidx] + if ok { + if assetDelta.created { + return assetDelta.creator, nil + } + return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) + } + } else { + for offset > 0 { + offset-- + assetDelta, ok := au.assetDeltas[offset][aidx] + if ok { + if assetDelta.created { + return assetDelta.creator, nil + } + return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) + } + } + } + + // Check the database + return au.accountsq.lookupAssetCreator(aidx) +} + +func (au *accountUpdates) totals(rnd basics.Round) (totals AccountTotals, err error) { + offset, err := au.roundOffset(rnd) + if err != nil { + return + } + + totals = au.roundTotals[offset] + return +} + // initializeCaches fills up the accountUpdates cache with the most recent ~320 blocks func (au *accountUpdates) initializeCaches(lastBalancesRound, lastestBlockRound, writingCatchpointRound basics.Round) (catchpointBlockDigest crypto.Digest, err error) { var blk bookkeeping.Block var delta StateDelta + accLedgerEval := accountUpdatesLedgerEvaluator{ + au: au, + } + if lastBalancesRound < lastestBlockRound { + accLedgerEval.prevHeader, err = au.ledger.BlockHdr(lastBalancesRound) + if err != nil { + return + } + } + for lastBalancesRound < lastestBlockRound { next := lastBalancesRound + 1 @@ -648,17 +719,19 @@ func (au *accountUpdates) initializeCaches(lastBalancesRound, lastestBlockRound, return } - delta, err = au.ledger.trackerEvalVerified(blk) + delta, err = au.ledger.trackerEvalVerified(blk, &accLedgerEval) if err != nil { return } - au.newBlock(blk, delta) + au.newBlockImpl(blk, delta) lastBalancesRound = next if next == basics.Round(writingCatchpointRound) { catchpointBlockDigest = blk.Digest() } + + accLedgerEval.prevHeader = *delta.hdr } return } @@ -666,15 +739,12 @@ func (au *accountUpdates) initializeCaches(lastBalancesRound, lastestBlockRound, // initializeFromDisk performs the atomic operation of loading the accounts data information from disk // and preparing the accountUpdates for operation, including initlizating the commitSyncer goroutine. func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRound, lastestBlockRound basics.Round, err error) { - au.accountsMu.Lock() - defer au.accountsMu.Unlock() - au.dbs = l.trackerDB() au.log = l.trackerLog() au.ledger = l if au.initAccounts == nil { - err = fmt.Errorf("accountUpdates.loadFromDisk: initAccounts not set") + err = fmt.Errorf("accountUpdates.initializeFromDisk: initAccounts not set") return } diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 5a1f463109..d290fd7155 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -58,7 +58,7 @@ func (ml *mockLedgerForTracker) Latest() basics.Round { return basics.Round(len(ml.blocks)) - 1 } -func (ml *mockLedgerForTracker) trackerEvalVerified(blk bookkeeping.Block) (StateDelta, error) { +func (ml *mockLedgerForTracker) trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) { delta := StateDelta{ hdr: &bookkeeping.BlockHeader{}, } diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 4bcf87116a..fa0eda5843 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -60,8 +60,8 @@ func (wl *wrappedLedger) BlockHdr(rnd basics.Round) (bookkeeping.BlockHeader, er return wl.l.BlockHdr(rnd) } -func (wl *wrappedLedger) trackerEvalVerified(blk bookkeeping.Block) (StateDelta, error) { - return wl.l.trackerEvalVerified(blk) +func (wl *wrappedLedger) trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) { + return wl.l.trackerEvalVerified(blk, accUpdatesLedger) } func (wl *wrappedLedger) Latest() basics.Round { @@ -554,13 +554,13 @@ func checkTrackers(t *testing.T, wl *wrappedLedger, rnd basics.Round) (basics.Ro var minSave basics.Round var cleanTracker ledgerTracker var trackerType reflect.Type + wl.l.trackerMu.RLock() + defer wl.l.trackerMu.RUnlock() for _, trk := range wl.l.trackers.trackers { - wl.l.trackerMu.RLock() if au, ok := trk.(*accountUpdates); ok { au.waitAccountsWriting() minSave = trk.committedUpTo(rnd) au.waitAccountsWriting() - wl.l.trackerMu.RUnlock() if minSave < minMinSave { minMinSave = minSave } @@ -575,7 +575,6 @@ func checkTrackers(t *testing.T, wl *wrappedLedger, rnd basics.Round) (basics.Ro au.initialize(cfg, "", au.initProto, wl.l.accts.initAccounts) } else { minSave = trk.committedUpTo(rnd) - wl.l.trackerMu.RUnlock() if minSave < minMinSave { minMinSave = minSave } diff --git a/ledger/eval.go b/ledger/eval.go index 8d2765b84d..13f7892584 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -757,7 +757,7 @@ func validateTransaction(txn transactions.SignedTxn, block bookkeeping.Block, pr // Validate: eval(ctx, blk, true, txcache, executionPool) // AddBlock: eval(context.Background(), blk, false, nil, nil) // tracker: eval(context.Background(), blk, false, nil, nil) -func (l *Ledger) eval(ctx context.Context, blk bookkeeping.Block, validate bool, txcache VerifiedTxnCache, executionPool execpool.BacklogPool) (StateDelta, error) { +func eval(ctx context.Context, l ledgerForEvaluator, blk bookkeeping.Block, validate bool, txcache VerifiedTxnCache, executionPool execpool.BacklogPool) (StateDelta, error) { eval, err := startEvaluator(l, blk.BlockHeader, len(blk.Payset), validate, false) if err != nil { return StateDelta{}, err @@ -837,7 +837,7 @@ func (l *Ledger) eval(ctx context.Context, blk bookkeeping.Block, validate bool, // not a valid block (e.g., it has duplicate transactions, overspends some // account, etc). func (l *Ledger) Validate(ctx context.Context, blk bookkeeping.Block, txcache VerifiedTxnCache, executionPool execpool.BacklogPool) (*ValidatedBlock, error) { - delta, err := l.eval(ctx, blk, true, txcache, executionPool) + delta, err := eval(ctx, l, blk, true, txcache, executionPool) if err != nil { return nil, err } diff --git a/ledger/ledger.go b/ledger/ledger.go index 481fc9145c..be99754925 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -137,6 +137,9 @@ func OpenLedger( } func (l *Ledger) reloadLedger() error { + l.trackerMu.Lock() + defer l.trackerMu.Unlock() + // close first. l.trackers.close() if l.blockQ != nil { @@ -161,7 +164,7 @@ func (l *Ledger) reloadLedger() error { err = l.trackers.loadFromDisk(l) if err != nil { - err = fmt.Errorf("reloadLedger.loadFromDisk %v", err) + err = fmt.Errorf("reloadLedger.reloadLedger %v", err) return err } @@ -326,7 +329,7 @@ func (l *Ledger) GetLastCatchpointLabel() string { func (l *Ledger) GetAssetCreatorForRound(rnd basics.Round, assetIdx basics.AssetIndex) (basics.Address, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getAssetCreatorForRound(rnd, assetIdx) + return l.accts.GetAssetCreatorForRound(rnd, assetIdx) } // GetAssetCreator is like GetAssetCreatorForRound, but for the latest round @@ -334,7 +337,7 @@ func (l *Ledger) GetAssetCreatorForRound(rnd basics.Round, assetIdx basics.Asset func (l *Ledger) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getAssetCreatorForRound(l.blockQ.latest(), assetIdx) + return l.accts.GetAssetCreatorForRound(l.blockQ.latest(), assetIdx) } // ListAssets takes a maximum asset index and maximum result length, and @@ -354,7 +357,7 @@ func (l *Ledger) Lookup(rnd basics.Round, addr basics.Address) (basics.AccountDa defer l.trackerMu.RUnlock() // Intentionally apply (pending) rewards up to rnd. - data, err := l.accts.lookup(rnd, addr, true) + data, err := l.accts.Lookup(rnd, addr, true) if err != nil { return basics.AccountData{}, err } @@ -368,7 +371,7 @@ func (l *Ledger) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (ba l.trackerMu.RLock() defer l.trackerMu.RUnlock() - data, err := l.accts.lookup(rnd, addr, false) + data, err := l.accts.Lookup(rnd, addr, false) if err != nil { return basics.AccountData{}, err } @@ -380,7 +383,7 @@ func (l *Ledger) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (ba func (l *Ledger) Totals(rnd basics.Round) (AccountTotals, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.totals(rnd) + return l.accts.Totals(rnd) } func (l *Ledger) isDup(currentProto config.ConsensusParams, current basics.Round, firstValid basics.Round, lastValid basics.Round, txid transactions.Txid, txl txlease) (bool, error) { @@ -444,7 +447,7 @@ func (l *Ledger) BlockCert(rnd basics.Round) (blk bookkeeping.Block, cert agreem func (l *Ledger) AddBlock(blk bookkeeping.Block, cert agreement.Certificate) error { // passing nil as the verificationPool is ok since we've asking the evaluator to skip verification. - updates, err := l.eval(context.Background(), blk, false, nil, nil) + updates, err := eval(context.Background(), l, blk, false, nil, nil) if err != nil { return err } @@ -535,9 +538,10 @@ func (l *Ledger) trackerLog() logging.Logger { return l.log } -func (l *Ledger) trackerEvalVerified(blk bookkeeping.Block) (StateDelta, error) { +// trackerEvalVerified is used by the accountUpdates to reconstruct the StateDelta from a given block. +func (l *Ledger) trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) { // passing nil as the verificationPool is ok since we've asking the evaluator to skip verification. - delta, err := l.eval(context.Background(), blk, false, nil, nil) + delta, err := eval(context.Background(), accUpdatesLedger, blk, false, nil, nil) return delta, err } diff --git a/ledger/tracker.go b/ledger/tracker.go index 46bddf82c5..e443105bd0 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -85,7 +85,7 @@ type ledgerForTracker interface { trackerDB() dbPair blockDB() dbPair trackerLog() logging.Logger - trackerEvalVerified(bookkeeping.Block) (StateDelta, error) + trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) Latest() basics.Round Block(basics.Round) (bookkeeping.Block, error) From fbe38513468fa87ee89087e0187cec9b989f8ea3 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 25 Jun 2020 11:22:52 -0400 Subject: [PATCH 076/267] Correct unit test. --- ledger/acctupdates_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index d290fd7155..5ffc2a35fe 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -346,7 +346,7 @@ func TestAcctUpdatesFastUpdates(t *testing.T) { rewardLevel += rewardLevelDelta updates, totals := randomDeltasBalanced(1, accts[i-1], rewardLevel) - prevTotals, err := au.totals(basics.Round(i - 1)) + prevTotals, err := au.Totals(basics.Round(i - 1)) require.NoError(t, err) oldPool := accts[i-1][testPoolAddr] From ce42eb85f2a56b4c85e5e6836d4e44f33da237ee Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 25 Jun 2020 12:03:10 -0400 Subject: [PATCH 077/267] Fix few unit test bugs. --- ledger/acctupdates_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 5ffc2a35fe..990b505585 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -127,14 +127,14 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, lates latest := au.latest() require.Equal(t, latest, latestRnd) - _, err := au.totals(latest + 1) + _, err := au.Totals(latest + 1) require.Error(t, err) _, err = au.lookup(latest+1, randomAddress(), false) require.Error(t, err) if base > 0 { - _, err := au.totals(base - 1) + _, err := au.Totals(base - 1) require.Error(t, err) _, err = au.lookup(base-1, randomAddress(), false) @@ -186,7 +186,7 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, lates require.NoError(t, err) require.Equal(t, all, accts[rnd]) - totals, err := au.totals(rnd) + totals, err := au.Totals(rnd) require.NoError(t, err) require.Equal(t, totals.Online.Money.Raw, totalOnline) require.Equal(t, totals.Offline.Money.Raw, totalOffline) @@ -261,7 +261,7 @@ func TestAcctUpdates(t *testing.T) { rewardLevel += rewardLevelDelta updates, totals := randomDeltasBalanced(1, accts[i-1], rewardLevel) - prevTotals, err := au.totals(basics.Round(i - 1)) + prevTotals, err := au.Totals(basics.Round(i - 1)) require.NoError(t, err) oldPool := accts[i-1][testPoolAddr] @@ -436,7 +436,7 @@ func BenchmarkBalancesChanges(b *testing.B) { } updates, totals := randomDeltasBalanced(accountChanges, accts[i-1], rewardLevel) - prevTotals, err := au.totals(basics.Round(i - 1)) + prevTotals, err := au.Totals(basics.Round(i - 1)) require.NoError(b, err) oldPool := accts[i-1][testPoolAddr] @@ -566,7 +566,7 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { rewardLevel += rewardLevelDelta updates, totals := randomDeltasBalanced(1, accts[i-1], rewardLevel) - prevTotals, err := au.totals(basics.Round(i - 1)) + prevTotals, err := au.Totals(basics.Round(i - 1)) require.NoError(t, err) oldPool := accts[i-1][testPoolAddr] From 3db5c1547f9c1383d2a709c671e266c13f0323c8 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 25 Jun 2020 13:01:17 -0400 Subject: [PATCH 078/267] Improve documentation and GenesisHash implementation --- ledger/acctupdates.go | 224 ++++++++++++++++++++----------------- ledger/acctupdates_test.go | 7 ++ ledger/archival_test.go | 4 + ledger/ledger.go | 7 +- ledger/tracker.go | 2 + 5 files changed, 141 insertions(+), 103 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 9c42841f82..9c19f4f4cc 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -248,55 +248,16 @@ func (au *accountUpdates) close() { au.waitAccountsWriting() } +// Lookup returns the accound data for a given address at a given round. The withRewards indicates whether the +// rewards should be added to the AccountData before returning. Note that the function doesn't update the account with the rewards, +// even while it could return the AccoutData which represent the "rewarded" account data. func (au *accountUpdates) Lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() return au.lookup(rnd, addr, withRewards) } -func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { - offset, err := au.roundOffset(rnd) - if err != nil { - return - } - - offsetForRewards := offset - - defer func() { - if withRewards { - totals := au.roundTotals[offsetForRewards] - proto := au.protos[offsetForRewards] - data = data.WithUpdatedRewards(proto, totals.RewardsLevel) - } - }() - - // Check if this is the most recent round, in which case, we can - // use a cache of the most recent account state. - if offset == uint64(len(au.deltas)) { - macct, ok := au.accounts[addr] - if ok { - return macct.data, nil - } - } else { - // Check if the account has been updated recently. Traverse the deltas - // backwards to ensure that later updates take priority if present. - for offset > 0 { - offset-- - d, ok := au.deltas[offset][addr] - if ok { - return d.new, nil - } - } - } - - // No updates of this account in the in-memory deltas; use on-disk DB. - // The check in roundOffset() made sure the round is exactly the one - // present in the on-disk DB. As an optimization, we avoid creating - // a separate transaction here, and directly use a prepared SQL query - // against the database. - return au.accountsq.lookup(addr) -} - +// listAssets lists the assets by their asset index, limiring to the first maxResults func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.CreatableLocator, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() @@ -489,68 +450,18 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b return } +// newBlock is the accountUpdates implementation of the ledgerTracker interface. This is the "external" facing function +// which invokes the internal implementation after taking the lock. func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { au.accountsMu.Lock() defer au.accountsMu.Unlock() au.newBlockImpl(blk, delta) } -func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) { - proto := config.Consensus[blk.CurrentProtocol] - rnd := blk.Round() - - if rnd <= au.latest() { - // Duplicate, ignore. - return - } - - if rnd != au.latest()+1 { - au.log.Panicf("accountUpdates: newBlock %d too far in the future, dbRound %d, deltas %d", rnd, au.dbRound, len(au.deltas)) - } - au.deltas = append(au.deltas, delta.accts) - au.protos = append(au.protos, proto) - au.assetDeltas = append(au.assetDeltas, delta.assets) - au.roundDigest = append(au.roundDigest, blk.Digest()) - au.deltasAccum = append(au.deltasAccum, len(delta.accts)+au.deltasAccum[len(au.deltasAccum)-1]) - - var ot basics.OverflowTracker - newTotals := au.roundTotals[len(au.roundTotals)-1] - allBefore := newTotals.All() - newTotals.applyRewards(delta.hdr.RewardsLevel, &ot) - - for addr, data := range delta.accts { - newTotals.delAccount(proto, data.old, &ot) - newTotals.addAccount(proto, data.new, &ot) - - macct := au.accounts[addr] - macct.ndeltas++ - macct.data = data.new - au.accounts[addr] = macct - } - - for aidx, adelta := range delta.assets { - masset := au.assets[aidx] - masset.creator = adelta.creator - masset.created = adelta.created - masset.ndeltas++ - au.assets[aidx] = masset - } - - if ot.Overflowed { - au.log.Panicf("accountUpdates: newBlock %d overflowed totals", rnd) - } - allAfter := newTotals.All() - if allBefore != allAfter { - au.log.Panicf("accountUpdates: sum of money changed from %d to %d", allBefore.Raw, allAfter.Raw) - } - - au.roundTotals = append(au.roundTotals, newTotals) -} - +// Totals returns the totals for a given round func (au *accountUpdates) Totals(rnd basics.Round) (totals AccountTotals, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - return au.totals(rnd) } @@ -609,15 +520,28 @@ func (au *accountUpdates) getCatchpointStream(round basics.Round) (io.ReadCloser // functions below this line are all internal functions +// accountUpdatesLedgerEvaluator is a "ledger emulator" which is used *only* by initializeCaches, as a way to shortcut +// the locks taken by the real ledger object when making requests that are being served by the accountUpdates. +// Using this struct allow us to take the tracker lock *before* calling the loadFromDisk, and having the operation complete +// without taking any locks. Note that it's not only the locks performance that is gained : by having the loadFrom disk +// not requiring any external locks, we can safely take a trackers lock on the ledger during reloadLedger, which ensures +// that even during catchpoint catchup mode switch, we're still correctly protected by a mutex. type accountUpdatesLedgerEvaluator struct { - au *accountUpdates + // au is the associated accountUpdates structure which invoking the trackerEvalVerified function, passing this structure as input. + // the accountUpdatesLedgerEvaluator would access the underlying accountUpdates function directly, bypassing the balances mutex lock. + au *accountUpdates + // prevHeader is the previous header to the current one. The usage of this is only in the context of initializeCaches where we iteratively + // building the StateDelta, which requires a peek on the "previous" header information. prevHeader bookkeeping.BlockHeader } +// GenesisHash returns the genesis hash func (aul *accountUpdatesLedgerEvaluator) GenesisHash() crypto.Digest { - return aul.prevHeader.GenesisHash + return aul.au.ledger.GenesisHash() } +// BlockHdr returns the header of the given round. When the evaluator is running, it's only referring to the previous header, which is what we +// are providing here. Any attempt to access a different header would get denied. func (aul *accountUpdatesLedgerEvaluator) BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) { if r == aul.prevHeader.Round { return aul.prevHeader, nil @@ -625,10 +549,12 @@ func (aul *accountUpdatesLedgerEvaluator) BlockHdr(r basics.Round) (bookkeeping. return bookkeeping.BlockHeader{}, ErrNoEntry{} } +// Lookup returns the account balance for a given address at a given round func (aul *accountUpdatesLedgerEvaluator) Lookup(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { return aul.au.lookup(rnd, addr, true) } +// Totals returns the totals for a given round func (aul *accountUpdatesLedgerEvaluator) Totals(rnd basics.Round) (AccountTotals, error) { return aul.au.totals(rnd) } @@ -638,15 +564,18 @@ func (aul *accountUpdatesLedgerEvaluator) isDup(config.ConsensusParams, basics.R return false, fmt.Errorf("accountUpdatesLedgerEvaluator: tried to check for dup during accountUpdates initilization ") } +// GetRoundTxIds returns the transaction ids for a given round. It's not being used by the evaluator at all, and should be (future task) be removed from +// the ledgerForEvaluator interface func (aul *accountUpdatesLedgerEvaluator) GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) { - // this is a non-issue since this call will never be made on non-validating evaluation return nil } +// Lookup returns the account balance for a given address at a given round, without the reward func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { return aul.au.lookup(rnd, addr, false) } +// GetAssetCreatorForRound returns the asset creator for a given round func (aul *accountUpdatesLedgerEvaluator) GetAssetCreatorForRound(rnd basics.Round, assetIdx basics.AssetIndex) (basics.Address, error) { return aul.au.getAssetCreatorForRound(rnd, assetIdx) } @@ -757,7 +686,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo } // Check for blocks DB and tracker DB un-sync if au.dbRound > lastestBlockRound { - au.log.Warnf("resetting accounts DB (on round %v, but blocks DB's latest is %v)", au.dbRound, lastestBlockRound) + au.log.Warnf("accountUpdates.initializeFromDisk: resetting accounts DB (on round %v, but blocks DB's latest is %v)", au.dbRound, lastestBlockRound) err0 = accountsReset(tx) if err0 != nil { return err0 @@ -944,6 +873,101 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Addre return } +func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) { + proto := config.Consensus[blk.CurrentProtocol] + rnd := blk.Round() + + if rnd <= au.latest() { + // Duplicate, ignore. + return + } + + if rnd != au.latest()+1 { + au.log.Panicf("accountUpdates: newBlock %d too far in the future, dbRound %d, deltas %d", rnd, au.dbRound, len(au.deltas)) + } + au.deltas = append(au.deltas, delta.accts) + au.protos = append(au.protos, proto) + au.assetDeltas = append(au.assetDeltas, delta.assets) + au.roundDigest = append(au.roundDigest, blk.Digest()) + au.deltasAccum = append(au.deltasAccum, len(delta.accts)+au.deltasAccum[len(au.deltasAccum)-1]) + + var ot basics.OverflowTracker + newTotals := au.roundTotals[len(au.roundTotals)-1] + allBefore := newTotals.All() + newTotals.applyRewards(delta.hdr.RewardsLevel, &ot) + + for addr, data := range delta.accts { + newTotals.delAccount(proto, data.old, &ot) + newTotals.addAccount(proto, data.new, &ot) + + macct := au.accounts[addr] + macct.ndeltas++ + macct.data = data.new + au.accounts[addr] = macct + } + + for aidx, adelta := range delta.assets { + masset := au.assets[aidx] + masset.creator = adelta.creator + masset.created = adelta.created + masset.ndeltas++ + au.assets[aidx] = masset + } + + if ot.Overflowed { + au.log.Panicf("accountUpdates: newBlock %d overflowed totals", rnd) + } + allAfter := newTotals.All() + if allBefore != allAfter { + au.log.Panicf("accountUpdates: sum of money changed from %d to %d", allBefore.Raw, allAfter.Raw) + } + + au.roundTotals = append(au.roundTotals, newTotals) +} + +func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { + offset, err := au.roundOffset(rnd) + if err != nil { + return + } + + offsetForRewards := offset + + defer func() { + if withRewards { + totals := au.roundTotals[offsetForRewards] + proto := au.protos[offsetForRewards] + data = data.WithUpdatedRewards(proto, totals.RewardsLevel) + } + }() + + // Check if this is the most recent round, in which case, we can + // use a cache of the most recent account state. + if offset == uint64(len(au.deltas)) { + macct, ok := au.accounts[addr] + if ok { + return macct.data, nil + } + } else { + // Check if the account has been updated recently. Traverse the deltas + // backwards to ensure that later updates take priority if present. + for offset > 0 { + offset-- + d, ok := au.deltas[offset][addr] + if ok { + return d.new, nil + } + } + } + + // No updates of this account in the in-memory deltas; use on-disk DB. + // The check in roundOffset() made sure the round is exactly the one + // present in the on-disk DB. As an optimization, we avoid creating + // a separate transaction here, and directly use a prepared SQL query + // against the database. + return au.accountsq.lookup(addr) +} + func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Round, totals AccountTotals, ledgerBlockDigest crypto.Digest, trieBalancesHash crypto.Digest) (label string, err error) { cpLabel := makeCatchpointLabel(committedRound, ledgerBlockDigest, trieBalancesHash, totals) label = cpLabel.String() diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 990b505585..c4ac13e73e 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -93,6 +93,13 @@ func (ml *mockLedgerForTracker) trackerLog() logging.Logger { return ml.log } +func (ml *mockLedgerForTracker) GenesisHash() crypto.Digest { + if len(ml.blocks) > 0 { + return ml.blocks[0].block.GenesisHash() + } + return crypto.Digest{} +} + // this function used to be in acctupdates.go, but we were never using it for production purposes. This // function has a conceptual flaw in that it attempts to load the entire balances into memory. This might // not work if we have large number of balances. On these unit testing, however, it's not the case, and it's diff --git a/ledger/archival_test.go b/ledger/archival_test.go index fa0eda5843..a5eaf19de9 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -80,6 +80,10 @@ func (wl *wrappedLedger) trackerLog() logging.Logger { return wl.l.trackerLog() } +func (wl *wrappedLedger) GenesisHash() crypto.Digest { + return wl.l.GenesisHash() +} + func getInitState() (genesisInitState InitState) { blk := bookkeeping.Block{} blk.CurrentProtocol = protocol.ConsensusCurrentVersion diff --git a/ledger/ledger.go b/ledger/ledger.go index be99754925..2ba16aaa23 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -538,11 +538,12 @@ func (l *Ledger) trackerLog() logging.Logger { return l.log } -// trackerEvalVerified is used by the accountUpdates to reconstruct the StateDelta from a given block. +// trackerEvalVerified is used by the accountUpdates to reconstruct the StateDelta from a given block during it's loadFromDisk execution. +// when this function is called, the trackers mutex is expected alredy to be taken. The provided accUpdatesLedger would allow the +// evaluator to shortcut the "main" ledger ( i.e. this struct ) and avoid taking the trackers lock a second time. func (l *Ledger) trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) { // passing nil as the verificationPool is ok since we've asking the evaluator to skip verification. - delta, err := eval(context.Background(), accUpdatesLedger, blk, false, nil, nil) - return delta, err + return eval(context.Background(), accUpdatesLedger, blk, false, nil, nil) } // A txlease is a transaction (sender, lease) pair which uniquely specifies a diff --git a/ledger/tracker.go b/ledger/tracker.go index e443105bd0..3a6c19cba2 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -20,6 +20,7 @@ import ( "fmt" "reflect" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/logging" @@ -90,6 +91,7 @@ type ledgerForTracker interface { Latest() basics.Round Block(basics.Round) (bookkeeping.Block, error) BlockHdr(basics.Round) (bookkeeping.BlockHeader, error) + GenesisHash() crypto.Digest } type trackerRegistry struct { From 40e0e38e73a3f15b81ca1c2b703ba90d444991d7 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 25 Jun 2020 13:05:38 -0400 Subject: [PATCH 079/267] update coding style. --- ledger/tracker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/tracker.go b/ledger/tracker.go index 3a6c19cba2..91cabfe260 100644 --- a/ledger/tracker.go +++ b/ledger/tracker.go @@ -86,7 +86,7 @@ type ledgerForTracker interface { trackerDB() dbPair blockDB() dbPair trackerLog() logging.Logger - trackerEvalVerified(blk bookkeeping.Block, accUpdatesLedger ledgerForEvaluator) (StateDelta, error) + trackerEvalVerified(bookkeeping.Block, ledgerForEvaluator) (StateDelta, error) Latest() basics.Round Block(basics.Round) (bookkeeping.Block, error) From 0dd78aeb17c7c9a72162b03eba3c7faf0efb3c84 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Thu, 25 Jun 2020 14:10:54 -0400 Subject: [PATCH 080/267] Update configure dev script to work with ubuntu 20.04 (#1199) python3.7-venv turned into python3.8-venv in ubuntu 20.04, use the default version instead. --- scripts/buildnumber.py | 2 +- scripts/configure_dev.sh | 2 +- test/scripts/timeout | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/buildnumber.py b/scripts/buildnumber.py index 42333bc1bc..59de8a076b 100755 --- a/scripts/buildnumber.py +++ b/scripts/buildnumber.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Computes a build number that is: # D[DD]HH diff --git a/scripts/configure_dev.sh b/scripts/configure_dev.sh index 4c9df6ea05..cfb8757717 100755 --- a/scripts/configure_dev.sh +++ b/scripts/configure_dev.sh @@ -49,7 +49,7 @@ if [ "${OS}" = "linux" ]; then fi sudo apt-get update - sudo apt-get install -y libboost-all-dev expect jq autoconf shellcheck sqlite3 python3.7-venv + sudo apt-get install -y libboost-all-dev expect jq autoconf shellcheck sqlite3 python3-venv elif [ "${OS}" = "darwin" ]; then brew update brew tap homebrew/cask diff --git a/test/scripts/timeout b/test/scripts/timeout index 3fe4bcd767..70b9366589 100755 --- a/test/scripts/timeout +++ b/test/scripts/timeout @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # # Usage: # timeout subcommand args... From 566855e77d3d5600eeeb6016fa30c607e6e02864 Mon Sep 17 00:00:00 2001 From: algoradam <37638838+algoradam@users.noreply.github.com> Date: Thu, 25 Jun 2020 20:37:36 +0000 Subject: [PATCH 081/267] Remove old buggy version of proposal LowestOutput (#1094) Now that the protocol upgrade fixing the bug has gone through, we no longer need to keep the buggy version around for compatibility. --- agreement/player_test.go | 13 +---------- agreement/proposalTracker.go | 20 ++++------------ agreement/proposalTracker_test.go | 8 +++---- config/consensus.go | 6 ----- data/committee/credential.go | 39 ------------------------------- 5 files changed, 10 insertions(+), 76 deletions(-) diff --git a/agreement/player_test.go b/agreement/player_test.go index 719d04ee81..c53a903004 100644 --- a/agreement/player_test.go +++ b/agreement/player_test.go @@ -24,20 +24,10 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/data/committee" //TODO(upgrade) remove this line "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" ) -// TODO(upgrade) remove the entire lessMaybeBuggy function once the upgrade goes through -func lessMaybeBuggy(cred, other committee.Credential) bool { - // this function calls either Less or LessBuggy depending on ConsensusCurrentVersion, which is what the agreement tests use - if config.Consensus[protocol.ConsensusCurrentVersion].UseBuggyProposalLowestOutput { - return cred.LessBuggy(other) - } - return cred.Less(other) -} - var playerTracer tracer func init() { @@ -69,8 +59,7 @@ func generateProposalEvents(t *testing.T, player player, accs testAccountData, f lowestCredential := votes[0].Cred lowestProposal = votes[0].R.Proposal for _, vote := range votes { - // if vote.Cred.Less(lowestCredential) { //TODO(upgrade) uncomment this line - if lessMaybeBuggy(vote.Cred, lowestCredential) { // TODO(upgrade) remove this line + if vote.Cred.Less(lowestCredential) { lowestCredential = vote.Cred lowestProposal = vote.R.Proposal } diff --git a/agreement/proposalTracker.go b/agreement/proposalTracker.go index add3c94480..72544ccb63 100644 --- a/agreement/proposalTracker.go +++ b/agreement/proposalTracker.go @@ -19,7 +19,6 @@ package agreement import ( "fmt" - "github.com/algorand/go-algorand/config" // TODO(upgrade): Please remove this line after the upgrade goes through "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/logging" ) @@ -38,23 +37,14 @@ type proposalSeeker struct { // accept compares a given vote with the current lowest-credentialled vote and // sets it if freeze has not been called. -// TODO(upgrade): Please remove the "useBuggyLowestOutput" argument as soon as the protocol upgrade goes through -func (s proposalSeeker) accept(v vote, useBuggyLowestOutput bool) (proposalSeeker, error) { +func (s proposalSeeker) accept(v vote) (proposalSeeker, error) { if s.Frozen { return s, errProposalSeekerFrozen{} } - // TODO(upgrade): Please remove the lines below as soon as the upgrade goes through - if useBuggyLowestOutput { - if s.Filled && !v.Cred.LessBuggy(s.Lowest.Cred) { - return s, errProposalSeekerNotLess{NewSender: v.R.Sender, LowestSender: s.Lowest.R.Sender} - } - } else { - // TODO(upgrade): Please remove the lines above as soon as the upgrade goes through - if s.Filled && !v.Cred.Less(s.Lowest.Cred) { - return s, errProposalSeekerNotLess{NewSender: v.R.Sender, LowestSender: s.Lowest.R.Sender} - } - } // TODO(upgrade): Please remove this line when the upgrade goes through + if s.Filled && !v.Cred.Less(s.Lowest.Cred) { + return s, errProposalSeekerNotLess{NewSender: v.R.Sender, LowestSender: s.Lowest.R.Sender} + } s.Lowest = v s.Filled = true @@ -156,7 +146,7 @@ func (t *proposalTracker) handle(r routerHandle, p player, e event) event { } var err error - t.Freezer, err = t.Freezer.accept(v, config.Consensus[e.Proto.Version].UseBuggyProposalLowestOutput) // TODO(upgrade): Please remove the second argument as soon as the upgrade goes through + t.Freezer, err = t.Freezer.accept(v) if err != nil { err := errProposalTrackerPS{Sub: err} return filteredEvent{T: voteFiltered, Err: makeSerErr(err)} diff --git a/agreement/proposalTracker_test.go b/agreement/proposalTracker_test.go index 84b23fb237..67e194f3d2 100644 --- a/agreement/proposalTracker_test.go +++ b/agreement/proposalTracker_test.go @@ -62,19 +62,19 @@ func TestProposalTrackerProposalSeeker(t *testing.T) { assert.False(t, s.Filled) // issue events in the following order: 2, 3, 1, (freeze), 0 - s, err = s.accept(votes[2], false) //TODO(upgrade) delete the ", false" + s, err = s.accept(votes[2]) assert.NoError(t, err) assert.False(t, s.Frozen) assert.True(t, s.Filled) assert.True(t, s.Lowest.equals(votes[2])) - s, err = s.accept(votes[3], false) //TODO(upgrade) delete the ", false" + s, err = s.accept(votes[3]) assert.Error(t, err) assert.False(t, s.Frozen) assert.True(t, s.Filled) assert.True(t, s.Lowest.equals(votes[2])) - s, err = s.accept(votes[1], false) //TODO(upgrade) delete the ", false" + s, err = s.accept(votes[1]) assert.NoError(t, err) assert.False(t, s.Frozen) assert.True(t, s.Filled) @@ -85,7 +85,7 @@ func TestProposalTrackerProposalSeeker(t *testing.T) { assert.True(t, s.Filled) assert.True(t, s.Lowest.equals(votes[1])) - s, err = s.accept(votes[0], false) //TODO(upgrade) delete the ", false" + s, err = s.accept(votes[0]) assert.Error(t, err) assert.True(t, s.Frozen) assert.True(t, s.Filled) diff --git a/config/consensus.go b/config/consensus.go index dcc675bf73..c2eb5fa2ae 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -211,10 +211,6 @@ type ConsensusParams struct { // max decimal precision for assets MaxAssetDecimals uint32 - // whether to use the old buggy Credential.lowestOutput function - // TODO(upgrade): Please remove as soon as the upgrade goes through - UseBuggyProposalLowestOutput bool - // SupportRekeying indicates support for account rekeying (the RekeyTo and AuthAddr fields) SupportRekeying bool @@ -501,7 +497,6 @@ func initConsensusProtocols() { MaxBalLookback: 320, MaxTxGroupSize: 1, - UseBuggyProposalLowestOutput: true, // TODO(upgrade): Please remove as soon as the upgrade goes through } v7.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} @@ -670,7 +665,6 @@ func initConsensusProtocols() { // v21 fixes a bug in Credential.lowestOutput that would cause larger accounts to be selected to propose disproportionately more often than small accounts v21 := v20 v21.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} - v21.UseBuggyProposalLowestOutput = false // TODO(upgrade): Please remove this line as soon as the protocol upgrade goes through Consensus[protocol.ConsensusV21] = v21 // v20 can be upgraded to v21. v20.ApprovedUpgrades[protocol.ConsensusV21] = 0 diff --git a/data/committee/credential.go b/data/committee/credential.go index df33e62035..de6a798f98 100644 --- a/data/committee/credential.go +++ b/data/committee/credential.go @@ -206,45 +206,6 @@ func (cred Credential) lowestOutput() *big.Int { return &lowest } -// TODO(upgrade): Please remove the entire lowestOutputBuggy function as soon as the corresponding protocol upgrade goes through. -func (cred Credential) lowestOutputBuggy() *big.Int { - var lowest big.Int - - h1 := cred.VrfOut - for i := uint64(0); i < cred.Weight; i++ { - var h crypto.Digest - if cred.DomainSeparationEnabled { - cred.Hashable.Iter = i - h = crypto.HashObj(cred.Hashable) - } else { - var h2 crypto.Digest - binary.BigEndian.PutUint64(h2[:], i) - h = crypto.Hash(append(h1[:], h2[:]...)) - } - - if i == 0 { - lowest.SetBytes(h[:]) - } else { - var temp big.Int - temp.SetBytes(h[:]) - if temp.Cmp(&lowest) < 0 { - lowest.Set(&temp) - } - } - } - - return &lowest -} - -// LessBuggy is the buggy version of Less -// TODO(upgrade): Please remove the entire LessBuggy function as soon as the corresponding protocol upgrade goes through -func (cred Credential) LessBuggy(otherCred Credential) bool { - i1 := cred.lowestOutputBuggy() - i2 := otherCred.lowestOutputBuggy() - - return i1.Cmp(i2) < 0 -} - // LowestOutputDigest gives the lowestOutput as a crypto.Digest, which allows // pretty-printing a proposal's lowest output. // This function is only used for debugging. From 5e022793ed888d9f394468329ef8b25ebb2408d6 Mon Sep 17 00:00:00 2001 From: John Lee Date: Fri, 26 Jun 2020 10:48:58 -0400 Subject: [PATCH 082/267] Match go modules in dev deps to versions in go.mod (#1206) The script to configure dev dependencies installs various supporting golang modules. However, these versions do not take into account entries in go.mod, which means the versions can conflict. This is a restoration of a previous pull request, with the bug fix around handling error conditions. --- scripts/configure_dev-deps.sh | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/scripts/configure_dev-deps.sh b/scripts/configure_dev-deps.sh index 6aa1caf853..59083ed004 100755 --- a/scripts/configure_dev-deps.sh +++ b/scripts/configure_dev-deps.sh @@ -2,11 +2,26 @@ set -ex +function get_go_version { + ( + cd $(dirname "$0") + VERSION=$(cat ../go.mod | grep "$1" 2>/dev/null | awk -F " " '{print $2}') + echo $VERSION + ) + return +} + function install_go_module { local OUTPUT - OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) - if [ "${OUTPUT}" != "" ]; then - echo "error: executing \"go get -u $1\" failed : ${OUTPUT}" + # Check for version to go.mod version + VERSION=$(get_go_version "$1") + if [ -z "$VERSION" ]; then + OUTPUT=$(GO111MODULE=off go get -u "$1" 2>&1) + else + OUTPUT=$(cd && GO111MODULE=on go get "$1@${VERSION}" 2>&1) + fi + if [ $? != 0 ]; then + echo "error: executing \"go get $1\" failed : ${OUTPUT}" exit 1 fi } From d787f3d294d46684fef2e6d8f7104e80a37e9f21 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 26 Jun 2020 18:22:51 -0400 Subject: [PATCH 083/267] Improve network connectionPerformanceMonitor performance (#1115) The connectionPerformanceMonitor is suspected to slow down critical communication paths. I was able to optimize it further and it looks now quite decent. ( i.e. 170Kns->6Kns ) The reminder of the 5Kns is mainly attributed to the deadlock locking logic, which I'm inclined to change in the PR unless proven it's really needed. --- network/connPerfMon.go | 86 +++++++++++++------ network/connPerfMon_test.go | 156 ++++++++++++++++++++++++++++++++++ network/messageFilter_test.go | 31 +++++++ 3 files changed, 247 insertions(+), 26 deletions(-) create mode 100644 network/connPerfMon_test.go create mode 100644 network/messageFilter_test.go diff --git a/network/connPerfMon.go b/network/connPerfMon.go index 896bd774c8..f2ad7e957a 100644 --- a/network/connPerfMon.go +++ b/network/connPerfMon.go @@ -45,6 +45,7 @@ const ( pmMaxMessageWaitTime = 15 * time.Second pmUndeliveredMessagePenaltyTime = 5 * time.Second pmDesiredMessegeDelayThreshold = 50 * time.Millisecond + pmMessageBucketDuration = time.Second ) // pmMessage is the internal storage for a single message. We save the time the message arrived from each of the peers. @@ -66,21 +67,28 @@ type pmStatistics struct { messageCount int64 // the number of messages used to calculate the above statistics } +// pmPendingMessageBucket is used to buffer messages in time ranges blocks. +type pmPendingMessageBucket struct { + messages map[crypto.Digest]*pmMessage // the pendingMessages map contains messages that haven't been received from all the peers within the pmMaxMessageWaitTime, and belong to the timerange of this bucket. + startTime int64 // the inclusive start-range of the timestamp which bounds the messages ranges which would go into this bucket. Time is in nano seconds UTC epoch time. + endTime int64 // the inclusive end-range of the timestamp which bounds the messages ranges which would go into this bucket. Time is in nano seconds UTC epoch time. +} + // connectionPerformanceMonitor is the connection monitor datatype. We typically would like to have a single monitor for all // the outgoing connections. type connectionPerformanceMonitor struct { deadlock.Mutex - monitoredConnections map[Peer]bool // the map of the connection we're going to monitor. Messages coming from other connections would be ignored. - monitoredMessageTags map[Tag]bool // the map of the message tags we're interested in monitoring. Messages that aren't broadcast-type typically would be a good choice here. - stage pmStage // the performance monitoring stage. - peerLastMsgTime map[Peer]int64 // the map describing the last time we received a message from each of the peers. - lastIncomingMsgTime int64 // the time at which the last message was received from any of the peers. - stageStartTime int64 // the timestamp at which we switched to the current stage. - pendingMessages map[crypto.Digest]*pmMessage // the pendingMessages map contains messages that haven't been received from all the peers within the pmMaxMessageWaitTime - connectionDelay map[Peer]int64 // contains the total delay we've sustained by each peer when we're in stages pmStagePresync-pmStageStopping and the average delay after that. ( in nano seconds ) - firstMessageCount map[Peer]int64 // maps the peers to their accumulated first messages ( the number of times a message seen coming from this peer first ) - msgCount int64 // total number of messages that we've accumulated. - accumulationTime int64 // the duration of which we're going to accumulate messages. This will get randomized to prevent cross-node syncronization. + monitoredConnections map[Peer]bool // the map of the connection we're going to monitor. Messages coming from other connections would be ignored. + monitoredMessageTags map[Tag]bool // the map of the message tags we're interested in monitoring. Messages that aren't broadcast-type typically would be a good choice here. + stage pmStage // the performance monitoring stage. + peerLastMsgTime map[Peer]int64 // the map describing the last time we received a message from each of the peers. + lastIncomingMsgTime int64 // the time at which the last message was received from any of the peers. + stageStartTime int64 // the timestamp at which we switched to the current stage. + pendingMessagesBuckets []*pmPendingMessageBucket // the pendingMessagesBuckets array contains messages buckets for messages that haven't been received from all the peers within the pmMaxMessageWaitTime + connectionDelay map[Peer]int64 // contains the total delay we've sustained by each peer when we're in stages pmStagePresync-pmStageStopping and the average delay after that. ( in nano seconds ) + firstMessageCount map[Peer]int64 // maps the peers to their accumulated first messages ( the number of times a message seen coming from this peer first ) + msgCount int64 // total number of messages that we've accumulated. + accumulationTime int64 // the duration of which we're going to accumulate messages. This will get randomized to prevent cross-node syncronization. } // makeConnectionPerformanceMonitor creates a new performance monitor instance, that is configured for monitoring the given message tags. @@ -145,7 +153,6 @@ func (pm *connectionPerformanceMonitor) ComparePeers(peers []Peer) bool { func (pm *connectionPerformanceMonitor) Reset(peers []Peer) { pm.Lock() defer pm.Unlock() - pm.pendingMessages = make(map[crypto.Digest]*pmMessage, 0) pm.monitoredConnections = make(map[Peer]bool, len(peers)) pm.peerLastMsgTime = make(map[Peer]int64, len(peers)) pm.connectionDelay = make(map[Peer]int64, len(peers)) @@ -265,7 +272,7 @@ func (pm *connectionPerformanceMonitor) notifyAccumulate(msg *IncomingMessage) { func (pm *connectionPerformanceMonitor) notifyStopping(msg *IncomingMessage) { pm.accumulateMessage(msg, false) pm.pruneOldMessages(msg.Received) - if len(pm.pendingMessages) > 0 { + if len(pm.pendingMessagesBuckets) > 0 { return } // time to wrap up. @@ -298,31 +305,58 @@ func (pm *connectionPerformanceMonitor) updateMessageIdlingInterval(now int64) ( func (pm *connectionPerformanceMonitor) pruneOldMessages(now int64) { oldestMessage := now - int64(pmMaxMessageWaitTime) - for digest, pendingMsg := range pm.pendingMessages { - if oldestMessage < pendingMsg.firstPeerTime { + prunedBucketsCount := 0 + for bucketIdx, currentMsgBucket := range pm.pendingMessagesBuckets { + if currentMsgBucket.endTime > oldestMessage { + pm.pendingMessagesBuckets[bucketIdx-prunedBucketsCount] = currentMsgBucket continue } - for peer := range pm.monitoredConnections { - if msgTime, hasPeer := pendingMsg.peerMsgTime[peer]; hasPeer { - msgDelayInterval := msgTime - pendingMsg.firstPeerTime - pm.connectionDelay[peer] += msgDelayInterval - } else { - // we never received this message from this peer. - pm.connectionDelay[peer] += int64(pmUndeliveredMessagePenaltyTime) + for _, pendingMsg := range currentMsgBucket.messages { + for peer := range pm.monitoredConnections { + if msgTime, hasPeer := pendingMsg.peerMsgTime[peer]; hasPeer { + msgDelayInterval := msgTime - pendingMsg.firstPeerTime + pm.connectionDelay[peer] += msgDelayInterval + } else { + // we never received this message from this peer. + pm.connectionDelay[peer] += int64(pmUndeliveredMessagePenaltyTime) + } } } - delete(pm.pendingMessages, digest) + prunedBucketsCount++ } + pm.pendingMessagesBuckets = pm.pendingMessagesBuckets[:len(pm.pendingMessagesBuckets)-prunedBucketsCount] } func (pm *connectionPerformanceMonitor) accumulateMessage(msg *IncomingMessage, newMessages bool) { msgDigest := generateMessageDigest(msg.Tag, msg.Data) - pendingMsg := pm.pendingMessages[msgDigest] + var msgBucket *pmPendingMessageBucket + var pendingMsg *pmMessage + var msgFound bool + // try to find the message. It's more likely to be found in the most recent bucket, so start there and go backward. + for bucketIndex := range pm.pendingMessagesBuckets { + currentMsgBucket := pm.pendingMessagesBuckets[len(pm.pendingMessagesBuckets)-1-bucketIndex] + if pendingMsg, msgFound = currentMsgBucket.messages[msgDigest]; msgFound { + msgBucket = currentMsgBucket + break + } + if msg.Received >= currentMsgBucket.startTime && msg.Received <= currentMsgBucket.endTime { + msgBucket = currentMsgBucket + } + } if pendingMsg == nil { if newMessages { + if msgBucket == nil { + // no bucket was found. create one. + msgBucket = &pmPendingMessageBucket{ + messages: make(map[crypto.Digest]*pmMessage), + startTime: msg.Received - (msg.Received % int64(pmMessageBucketDuration)), // align with pmMessageBucketDuration + } + msgBucket.endTime = msgBucket.startTime + int64(pmMessageBucketDuration) - 1 + pm.pendingMessagesBuckets = append(pm.pendingMessagesBuckets, msgBucket) + } // we don't have this one yet, add it. - pm.pendingMessages[msgDigest] = &pmMessage{ + msgBucket.messages[msgDigest] = &pmMessage{ peerMsgTime: map[Peer]int64{ msg.Sender: msg.Received, }, @@ -346,6 +380,6 @@ func (pm *connectionPerformanceMonitor) accumulateMessage(msg *IncomingMessage, for peer, msgTime := range pendingMsg.peerMsgTime { pm.connectionDelay[peer] += msgTime - pendingMsg.firstPeerTime } - delete(pm.pendingMessages, msgDigest) + delete(msgBucket.messages, msgDigest) } } diff --git a/network/connPerfMon_test.go b/network/connPerfMon_test.go new file mode 100644 index 0000000000..8eceb4c620 --- /dev/null +++ b/network/connPerfMon_test.go @@ -0,0 +1,156 @@ +// 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 . + +package network + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/protocol" +) + +func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { + // preallocate enough. + out = make([]IncomingMessage, 0, N*2) + msgIndex := uint64(0) + timer := int64(0) + msgPerSecond := uint64(1500) + for { + if len(out) >= N { + break + } + + msgData := crypto.Hash([]byte{byte(msgIndex & 0xff), byte((msgIndex >> 8) & 0xff), byte((msgIndex >> 16) & 0xff), byte((msgIndex >> 24) & 0xff)}) + msg := IncomingMessage{ + Tag: protocol.AgreementVoteTag, + Data: msgData[:], + } + + addMsg := func(msgCount int) { + for i := 0; i < msgCount; i++ { + msg.Sender = peers[(int(msgIndex)+i)%len(peers)] + timer += int64(7 * time.Nanosecond) + msg.Received = timer + out = append(out, msg) + } + } + switch { + case (msgIndex % 10) == 0: // 10% of the messages comes from a single source + addMsg(1) + case (msgIndex%10) == 1 || (msgIndex%10) == 2: // 20% of the messages comes from two sources + addMsg(2) + case (msgIndex%10) == 3 || (msgIndex%10) == 4: // 20% of the messages comes from three sources + addMsg(3) + default: // 50% of the messages comes from all sources + addMsg(len(peers)) + } + + msgIndex++ + if msgIndex%msgPerSecond == 0 { + timer += int64(time.Second) + } + timer += int64(123 * time.Nanosecond) + } + return +} + +func BenchmarkConnMonitor(b *testing.B) { + peers := []Peer{&wsPeer{}, &wsPeer{}, &wsPeer{}, &wsPeer{}} + msgPool := makeMsgPool(b.N, peers) + + b.ResetTimer() + startTestTime := time.Now().UnixNano() + perfMonitor := makeConnectionPerformanceMonitor([]Tag{protocol.AgreementVoteTag}) + perfMonitor.Reset(peers) + for _, msg := range msgPool { + msg.Received += startTestTime + perfMonitor.Notify(&msg) + if perfMonitor.GetPeersStatistics() != nil { + perfMonitor.Reset(peers) + startTestTime = time.Now().UnixNano() + } + } +} + +func TestConnMonitorStageTiming(t *testing.T) { + peers := []Peer{&wsPeer{}, &wsPeer{}, &wsPeer{}, &wsPeer{}} + msgPool := makeMsgPool(1000000, peers) + + stageTimings := make([]time.Duration, 5) + stageNotifyCalls := make([]int, 5) + startTestTime := time.Now().UnixNano() + perfMonitor := makeConnectionPerformanceMonitor([]Tag{protocol.AgreementVoteTag}) + // measure measuring overhead. + measuringOverhead := time.Now().Sub(time.Now()) + perfMonitor.Reset(peers) + for msgIdx, msg := range msgPool { + msg.Received += startTestTime + beforeNotify := time.Now() + beforeNotifyStage := perfMonitor.stage + perfMonitor.Notify(&msg) + notifyTime := time.Now().Sub(beforeNotify) + stageTimings[beforeNotifyStage] += notifyTime + stageNotifyCalls[beforeNotifyStage]++ + if perfMonitor.GetPeersStatistics() != nil { + fmt.Printf("TestConnMonitorStageTiming is done after going over %d messages\n", msgIdx) + break + } + } + for i := 0; i < len(stageTimings); i++ { + if stageNotifyCalls[i] == 0 { + continue + } + fmt.Printf("ConnectionPerformanceMonitor stage %d had %d calls with avarage of %dns and total of %dns\n", + i, + stageNotifyCalls[i], + int64(stageTimings[i])/int64(stageNotifyCalls[i])-int64(measuringOverhead), + stageTimings[i]) + } + +} +func TestBucketsPruning(t *testing.T) { + bucketsCount := 100 + curTime := time.Now().UnixNano() + for i := 0; i < bucketsCount; i++ { + perfMonitor := makeConnectionPerformanceMonitor([]Tag{protocol.AgreementVoteTag}) + // create bucketsCount buckets, where i of them are before the "current" time stamp and bucketsCount-i are after the time stamp. + for j := 0; j < bucketsCount; j++ { + if j < i { + perfMonitor.pendingMessagesBuckets = append(perfMonitor.pendingMessagesBuckets, &pmPendingMessageBucket{endTime: curTime - 1}) + } else { + perfMonitor.pendingMessagesBuckets = append(perfMonitor.pendingMessagesBuckets, &pmPendingMessageBucket{endTime: curTime + 1}) + } + } + perfMonitor.pruneOldMessages(curTime + int64(pmMaxMessageWaitTime)) + require.Equal(t, bucketsCount-i, len(perfMonitor.pendingMessagesBuckets)) + } + + for i := 0; i < bucketsCount; i++ { + perfMonitor := makeConnectionPerformanceMonitor([]Tag{protocol.AgreementVoteTag}) + + for j := 0; j < bucketsCount; j++ { + perfMonitor.pendingMessagesBuckets = append(perfMonitor.pendingMessagesBuckets, &pmPendingMessageBucket{endTime: curTime + int64(j)}) + } + + perfMonitor.pruneOldMessages(curTime + int64(pmMaxMessageWaitTime) + int64(i-1)) + require.Equal(t, bucketsCount-i, len(perfMonitor.pendingMessagesBuckets)) + } +} diff --git a/network/messageFilter_test.go b/network/messageFilter_test.go new file mode 100644 index 0000000000..bdf017a119 --- /dev/null +++ b/network/messageFilter_test.go @@ -0,0 +1,31 @@ +// 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 . + +package network + +import ( + "testing" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/protocol" +) + +func BenchmarkGenerateMessageDigest(b *testing.B) { + for i := 0; i < b.N; i++ { + msgData := crypto.Hash([]byte{byte(i & 0xff), byte((i >> 8) & 0xff), byte((i >> 16) & 0xff), byte((i >> 24) & 0xff)}) + generateMessageDigest(protocol.AgreementVoteTag, msgData[:]) + } +} From d8c08a72614e785a7132d91044a32ca6c8cacf47 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 27 Jun 2020 16:00:36 -0400 Subject: [PATCH 084/267] Fix random catchpoint catchup expect test case (#1208) The test was creating a proxy which delays requests execution as a way to slow down the catchpoint catchup process. This is important so that we can monitor from the goal command that the catchup is working as intended. However, delaying the command execution caused an issue where the node was trying to issue multiple requests for blocks 1-16, in parallel, which reached the proxy at an arbitrary order. As a result, the request for block #1 was delayed by more than 4 second, causing it it timeout. The solution was to reconfigure the number of parallel blocks being retrieved to 2. This would ensure that we only getting two blocks at a time. Since the delay is configured to 1.5 seconds, this would also be the delay, which is well under 4 second. --- test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp index a5e6c03534..f09ec8d0a9 100755 --- a/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp +++ b/test/e2e-go/cli/goal/expect/catchpointCatchupTest.exp @@ -61,6 +61,12 @@ if { [catch { exec rm $TEST_ROOT_DIR/Primary/config.json exec mv $TEST_ROOT_DIR/Primary/config.json.new $TEST_ROOT_DIR/Primary/config.json + # Update the Second Node configuration + exec -- cat "$TEST_ROOT_DIR/Node/config.json" | jq {. |= . + {"CatchupParallelBlocks":2}} > $TEST_ROOT_DIR/Node/config.json.new + exec rm $TEST_ROOT_DIR/Node/config.json + exec mv $TEST_ROOT_DIR/Node/config.json.new $TEST_ROOT_DIR/Node/config.json + + set ::GLOBAL_TEST_ALGO_DIR $TEST_ALGO_DIR set ::GLOBAL_TEST_ROOT_DIR $TEST_ROOT_DIR set ::GLOBAL_NETWORK_NAME $NETWORK_NAME From 031b2d20ee97dc17826613f0590c1c58bbc5f989 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 29 Jun 2020 12:44:48 -0400 Subject: [PATCH 085/267] Fix random fails in TestCatchupOverGossip (#1209) TestCatchupOverGossip was using an incorrect verification and shown random failures. --- test/e2e-go/features/catchup/basicCatchup_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/e2e-go/features/catchup/basicCatchup_test.go b/test/e2e-go/features/catchup/basicCatchup_test.go index 12a2d04963..f61050e515 100644 --- a/test/e2e-go/features/catchup/basicCatchup_test.go +++ b/test/e2e-go/features/catchup/basicCatchup_test.go @@ -153,6 +153,7 @@ func runCatchupOverGossip(t *testing.T, err = fixture.LibGoalFixture.ClientWaitForRoundWithTimeout(lg, waitForRound) a.NoError(err) + waitStart := time.Now() // wait until the round number on the secondary node matches the round number on the primary node. for { nodeLibGoalClient := fixture.LibGoalFixture.GetLibGoalClientFromDataDir(nc.GetDataDir()) @@ -161,11 +162,16 @@ func runCatchupOverGossip(t *testing.T, primaryStatus, err := lg.Status() a.NoError(err) - a.True(nodeStatus.LastRound >= primaryStatus.LastRound) - if nodeStatus.LastRound == primaryStatus.LastRound && waitForRound < nodeStatus.LastRound { + if nodeStatus.LastRound <= primaryStatus.LastRound && waitForRound < nodeStatus.LastRound { //t.Logf("Both nodes reached round %d\n", primaryStatus.LastRound) break } + + if time.Now().Sub(waitStart) > time.Minute { + // it's taking too long. + require.FailNow(t, "Waiting too long for catchup to complete") + } + time.Sleep(50 * time.Millisecond) } } From 91287ba2fef65f0bddb793fd6f32dcc4674c0ddf Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 29 Jun 2020 20:43:12 -0400 Subject: [PATCH 086/267] Add e2e teal/compile test (#1210) Add an e2e teal/compile endpoint test --- daemon/algod/api/client/restClient.go | 24 ++++++ libgoal/teal.go | 30 ++++++++ test/e2e-go/features/teal/compile_test.go | 74 +++++++++++++++++++ test/testdata/nettemplates/OneNodeFuture.json | 23 ++++++ 4 files changed, 151 insertions(+) create mode 100644 libgoal/teal.go create mode 100644 test/e2e-go/features/teal/compile_test.go create mode 100644 test/testdata/nettemplates/OneNodeFuture.json diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index 4fc1e4316b..b36381d711 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -19,6 +19,7 @@ package client import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" @@ -32,8 +33,10 @@ import ( generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" privateV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated/private" + "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/spec/common" "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" ) @@ -57,6 +60,7 @@ const ( // rawRequestPaths is a set of paths where the body should not be urlencoded var rawRequestPaths = map[string]bool{ "/v1/transactions": true, + "/v2/teal/compile": true, } // RestClient manages the REST interface for a calling user. @@ -409,6 +413,26 @@ func (client RestClient) GetGoRoutines(ctx context.Context) (goRoutines string, return } +// Compile compiles the given program and returned the compiled program +func (client RestClient) Compile(program []byte) (compiledProgram []byte, programHash crypto.Digest, err error) { + var compileResponse generatedV2.PostCompileResponse + err = client.submitForm(&compileResponse, "/v2/teal/compile", program, "POST", false, true) + if err != nil { + return nil, crypto.Digest{}, err + } + compiledProgram, err = base64.StdEncoding.DecodeString(compileResponse.Result) + if err != nil { + return nil, crypto.Digest{}, err + } + var progAddr basics.Address + progAddr, err = basics.UnmarshalChecksumAddress(compileResponse.Hash) + if err != nil { + return nil, crypto.Digest{}, err + } + programHash = crypto.Digest(progAddr) + return +} + func (client RestClient) doGetWithQuery(ctx context.Context, path string, queryArgs map[string]string) (result string, err error) { queryURL := client.serverURL queryURL.Path = path diff --git a/libgoal/teal.go b/libgoal/teal.go new file mode 100644 index 0000000000..e81590f71c --- /dev/null +++ b/libgoal/teal.go @@ -0,0 +1,30 @@ +// 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 . + +package libgoal + +import ( + "github.com/algorand/go-algorand/crypto" +) + +// Compile compiles the given program and returned the compiled program +func (c *Client) Compile(program []byte) (compiledProgram []byte, compiledProgramHash crypto.Digest, err error) { + algod, err2 := c.ensureAlgodClient() + if err2 != nil { + return nil, crypto.Digest{}, err2 + } + return algod.Compile(program) +} diff --git a/test/e2e-go/features/teal/compile_test.go b/test/e2e-go/features/teal/compile_test.go new file mode 100644 index 0000000000..5b1f079bd2 --- /dev/null +++ b/test/e2e-go/features/teal/compile_test.go @@ -0,0 +1,74 @@ +// 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 . + +package teal + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/test/framework/fixtures" +) + +func TestTealCompile(t *testing.T) { + if testing.Short() { + t.Skip() + } + a := require.New(t) + + var fixture fixtures.RestClientFixture + fixture.SetupNoStart(t, filepath.Join("nettemplates", "OneNodeFuture.json")) + + // Get primary node + primaryNode, err := fixture.GetNodeController("Primary") + a.NoError(err) + + fixture.Start() + defer primaryNode.FullStop() + + // get lib goal client + libGoalClient := fixture.LibGoalFixture.GetLibGoalClientFromNodeController(primaryNode) + + compiledProgram, _, err := libGoalClient.Compile([]byte("")) + a.Nil(compiledProgram) + a.Equal(err.Error(), "HTTP 404 Not Found: /teal/compile was not enabled in the configuration file by setting the EnableDeveloperAPI to true") + + primaryNode.FullStop() + + // update the configuration file to enable the developer API + cfg, err := config.LoadConfigFromDisk(primaryNode.GetDataDir()) + a.NoError(err) + cfg.EnableDeveloperAPI = true + cfg.SaveToDisk(primaryNode.GetDataDir()) + + fixture.Start() + + var hash crypto.Digest + compiledProgram, hash, err = libGoalClient.Compile([]byte("int 1")) + a.NotNil(compiledProgram) + a.NoError(err, "A valid program should result in a compilation success") + a.Equal([]byte{0x2, 0x20, 0x1, 0x1, 0x22}, compiledProgram) + a.Equal("YOE6C22GHCTKAN3HU4SE5PGIPN5UKXAJTXCQUPJ3KKF5HOAH646A", hash.String()) + + compiledProgram, hash, err = libGoalClient.Compile([]byte("bad program")) + a.Error(err, "An invalid program should result in a compilation failure") + a.Nil(compiledProgram) + a.Equal(crypto.Digest{}, hash) +} diff --git a/test/testdata/nettemplates/OneNodeFuture.json b/test/testdata/nettemplates/OneNodeFuture.json new file mode 100644 index 0000000000..1bbe378745 --- /dev/null +++ b/test/testdata/nettemplates/OneNodeFuture.json @@ -0,0 +1,23 @@ +{ + "Genesis": { + "NetworkName": "tbd", + "ConsensusProtocol": "future", + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 100, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [ + { "Name": "Wallet1", + "ParticipationOnly": false } + ] + } + ] +} From decc8b23ed2c511ef35ffa7fcb5ba276ad780723 Mon Sep 17 00:00:00 2001 From: egieseke Date: Tue, 30 Jun 2020 13:51:18 -0400 Subject: [PATCH 087/267] Fix cicd perf test (#1207) Replace references to BUILDCHANNEL with CHANNEL to simplify scripts. --- Makefile | 10 +++++----- docker/build/Dockerfile | 1 - docker/build/Dockerfile-deploy | 1 - docker/build/cicd.centos.Dockerfile | 1 - docker/build/cicd.ubuntu.Dockerfile | 1 - docker/build/mule.go.centos.Dockerfile | 1 - scripts/deploy_linux_version.sh | 1 - scripts/deploy_private_version.sh | 1 - scripts/deploy_version.sh | 2 +- scripts/envDevnet.sh | 2 +- scripts/envTestnet.sh | 2 +- scripts/release/common/setup.sh | 2 +- scripts/release/mule/Makefile.mule | 16 ++++++++-------- 13 files changed, 17 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index 2d6b7da8ff..a0d7907c1e 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ S3_RELEASE_BUCKET = $$S3_RELEASE_BUCKET BUILDNUMBER ?= $(shell ./scripts/compute_build_number.sh) COMMITHASH := $(shell ./scripts/compute_build_commit.sh) BUILDBRANCH := $(shell ./scripts/compute_branch.sh) -BUILDCHANNEL := $(shell ./scripts/compute_branch_channel.sh $(BUILDBRANCH)) +CHANNEL ?= $(shell ./scripts/compute_branch_channel.sh $(BUILDBRANCH)) DEFAULTNETWORK ?= $(shell ./scripts/compute_branch_network.sh $(BUILDBRANCH)) DEFAULT_DEADLOCK ?= $(shell ./scripts/compute_branch_deadlock_default.sh $(BUILDBRANCH)) @@ -48,7 +48,7 @@ GOLDFLAGS_BASE := -X github.com/algorand/go-algorand/config.BuildNumber=$(BUILD -extldflags \"$(EXTLDFLAGS)\" GOLDFLAGS := $(GOLDFLAGS_BASE) \ - -X github.com/algorand/go-algorand/config.Channel=$(BUILDCHANNEL) + -X github.com/algorand/go-algorand/config.Channel=$(CHANNEL) UNIT_TEST_SOURCES := $(sort $(shell GO111MODULE=off go list ./... | grep -v /go-algorand/test/ )) ALGOD_API_PACKAGES := $(sort $(shell GO111MODULE=off cd daemon/algod/api; go list ./... )) @@ -292,6 +292,6 @@ include ./scripts/release/mule/Makefile.mule SUPPORTED_ARCHIVE_OS_ARCH = linux/amd64 linux/arm64 linux/arm darwin/amd64 archive: - CHANNEL=$(BUILDCHANNEL) \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$${PATH} \ - scripts/upload_version.sh $(BUILDCHANNEL) $(SRCPATH)/tmp/node_pkgs $(S3_RELEASE_BUCKET) + CHANNEL=$(CHANNEL) \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$${PATH} \ + scripts/upload_version.sh $(CHANNEL) $(SRCPATH)/tmp/node_pkgs $(S3_RELEASE_BUCKET) diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index ac62e8dd55..416b9ef0ee 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -13,7 +13,6 @@ COPY ./go-algorand ./go-algorand/ ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ BRANCH=${BRANCH} \ CHANNEL=${CHANNEL} \ - BUILDCHANNEL=${BUILDCHANNEL} \ DEFAULTNETWORK=${DEFAULTNETWORK} \ FULLVERSION=${FULLVERSION} \ PKG_ROOT=${PKG_ROOT} diff --git a/docker/build/Dockerfile-deploy b/docker/build/Dockerfile-deploy index cc534ceb63..18671309b9 100644 --- a/docker/build/Dockerfile-deploy +++ b/docker/build/Dockerfile-deploy @@ -13,7 +13,6 @@ COPY . ./go-algorand/ ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ BRANCH=${BRANCH} \ CHANNEL=${CHANNEL} \ - BUILDCHANNEL=${BUILDCHANNEL} \ DEFAULTNETWORK=${DEFAULTNETWORK} \ FULLVERSION=${FULLVERSION} \ PKG_ROOT=${PKG_ROOT} \ diff --git a/docker/build/cicd.centos.Dockerfile b/docker/build/cicd.centos.Dockerfile index 3021859e5a..4758116620 100644 --- a/docker/build/cicd.centos.Dockerfile +++ b/docker/build/cicd.centos.Dockerfile @@ -20,7 +20,6 @@ COPY . $GOPATH/src/github.com/algorand/go-algorand ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ BRANCH=${BRANCH} \ CHANNEL=${CHANNEL} \ - BUILDCHANNEL=${BUILDCHANNEL} \ DEFAULTNETWORK=${DEFAULTNETWORK} \ FULLVERSION=${FULLVERSION} \ GOPROXY=https://gocenter.io \ diff --git a/docker/build/cicd.ubuntu.Dockerfile b/docker/build/cicd.ubuntu.Dockerfile index 21bfe60451..53715c9e94 100644 --- a/docker/build/cicd.ubuntu.Dockerfile +++ b/docker/build/cicd.ubuntu.Dockerfile @@ -15,7 +15,6 @@ COPY . $GOPATH/src/github.com/algorand/go-algorand ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ BRANCH=${BRANCH} \ CHANNEL=${CHANNEL} \ - BUILDCHANNEL=${BUILDCHANNEL} \ DEFAULTNETWORK=${DEFAULTNETWORK} \ FULLVERSION=${FULLVERSION} \ GOPROXY=https://gocenter.io \ diff --git a/docker/build/mule.go.centos.Dockerfile b/docker/build/mule.go.centos.Dockerfile index 5c2c538f05..734a388dc5 100644 --- a/docker/build/mule.go.centos.Dockerfile +++ b/docker/build/mule.go.centos.Dockerfile @@ -18,7 +18,6 @@ COPY . $GOPATH/src/github.com/algorand/go-algorand ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH \ BRANCH=${BRANCH} \ CHANNEL=${CHANNEL} \ - BUILDCHANNEL=${BUILDCHANNEL} \ DEFAULTNETWORK=${DEFAULTNETWORK} \ FULLVERSION=${FULLVERSION} \ GOPROXY=https://gocenter.io \ diff --git a/scripts/deploy_linux_version.sh b/scripts/deploy_linux_version.sh index c1be6b2304..927e624d86 100755 --- a/scripts/deploy_linux_version.sh +++ b/scripts/deploy_linux_version.sh @@ -65,7 +65,6 @@ echo SCRIPTPATH='$( cd "$(dirname "$0")" ; pwd -P )' >> ${TMPDIR}/deploy_linux_v echo cd \${SCRIPTPATH}/..${RELPATHXTRA} >> ${TMPDIR}/deploy_linux_version_exec.sh echo export BRANCH=${BRANCH} >> ${TMPDIR}/deploy_linux_version_exec.sh echo export CHANNEL=${CHANNEL} >> ${TMPDIR}/deploy_linux_version_exec.sh -echo export BUILDCHANNEL=${BUILDCHANNEL} >> ${TMPDIR}/deploy_linux_version_exec.sh echo export DEFAULTNETWORK=${DEFAULTNETWORK} >> ${TMPDIR}/deploy_linux_version_exec.sh echo export GENESISFILE=${GENESISFILE} >> ${TMPDIR}/deploy_linux_version_exec.sh echo export FULLVERSION=${FULLVERSION} >> ${TMPDIR}/deploy_linux_version_exec.sh diff --git a/scripts/deploy_private_version.sh b/scripts/deploy_private_version.sh index 25e189e159..c212118560 100755 --- a/scripts/deploy_private_version.sh +++ b/scripts/deploy_private_version.sh @@ -83,7 +83,6 @@ fi export BRANCH=$(./scripts/compute_branch.sh) export CHANNEL=${CHANNEL} -export BUILDCHANNEL=${CHANNEL} export DEFAULTNETWORK=${DEFAULTNETWORK} export FULLVERSION=$(./scripts/compute_build_number.sh -f) export PKG_ROOT=${HOME}/node_pkg diff --git a/scripts/deploy_version.sh b/scripts/deploy_version.sh index f67feda88e..72bbef82d8 100755 --- a/scripts/deploy_version.sh +++ b/scripts/deploy_version.sh @@ -34,7 +34,7 @@ fi export BRANCH=$1 shift -export CHANNEL=$(./scripts/compute_branch_channel.sh ${BRANCH}) +export CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "${BRANCH}")} export FULLVERSION=$(./scripts/compute_build_number.sh -f) export PKG_ROOT=${HOME}/node_pkg diff --git a/scripts/envDevnet.sh b/scripts/envDevnet.sh index 89857bad91..7fb3123518 100755 --- a/scripts/envDevnet.sh +++ b/scripts/envDevnet.sh @@ -3,4 +3,4 @@ echo Configuring for devnet build unset DEFAULTNETWORK -unset BUILDCHANNEL +unset CHANNEL diff --git a/scripts/envTestnet.sh b/scripts/envTestnet.sh index 3ec4553f8d..794b31f485 100755 --- a/scripts/envTestnet.sh +++ b/scripts/envTestnet.sh @@ -3,4 +3,4 @@ echo Configuring for testnet build export DEFAULTNETWORK=testnet -export BUILDCHANNEL=stable +export CHANNEL=stable diff --git a/scripts/release/common/setup.sh b/scripts/release/common/setup.sh index 1901a947c6..53005f6da3 100755 --- a/scripts/release/common/setup.sh +++ b/scripts/release/common/setup.sh @@ -132,7 +132,7 @@ PLATFORM_SPLIT=(${PLATFORM//\// }) # a bash user might `source build_env` to manually continue a broken build cat << EOF > "${HOME}"/build_env export BRANCH=${BRANCH} -export CHANNEL=$("${GOPATH}"/src/github.com/algorand/go-algorand/scripts/compute_branch_channel.sh "${BRANCH}") +export CHANNEL=${CHANNEL:-$("${GOPATH}"/src/github.com/algorand/go-algorand/scripts/compute_branch_channel.sh "$BRANCH")} export COMMIT_HASH=${COMMIT_HASH} export DEFAULTNETWORK=$(PATH=${PATH} "${REPO_ROOT}"/scripts/compute_branch_network.sh) export DC_IP=$(curl --silent http://169.254.169.254/latest/meta-data/local-ipv4) diff --git a/scripts/release/mule/Makefile.mule b/scripts/release/mule/Makefile.mule index 3f8ba746cc..ce94e22aea 100644 --- a/scripts/release/mule/Makefile.mule +++ b/scripts/release/mule/Makefile.mule @@ -26,18 +26,18 @@ endif done ci-integration: - NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$$PATH \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/tools:$$PATH \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH \ + NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$$PATH \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/tools:$$PATH \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH \ SRCROOT=$(SRCPATH) \ - test/scripts/e2e.sh -c $(BUILDCHANNEL) -n + test/scripts/e2e.sh -c $(CHANNEL) -n ci-build: buildsrc gen ci-setup - CHANNEL=$(BUILDCHANNEL) PKG_ROOT=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) NO_BUILD=True VARIATIONS=$(OS_TYPE)-$(ARCH) \ + CHANNEL=$(CHANNEL) PKG_ROOT=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) NO_BUILD=True VARIATIONS=$(OS_TYPE)-$(ARCH) \ scripts/build_packages.sh $(OS_TYPE)/$(ARCH) && \ - mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/data && \ - cp gen/devnet/genesis.json $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(BUILDCHANNEL)/$(OS_TYPE)-$(ARCH)/data + mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/data && \ + cp gen/devnet/genesis.json $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/data # Builds targets from the sub-directories of ./scripts/release/mule/, such as `mule-package`, `mule-sign`, `mule-test`. # https://scene-si.org/2019/12/04/make-dynamic-makefile-targets/ From 9680c588c5c9127fbc5f82ad16a6a028c7355756 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 30 Jun 2020 20:06:38 -0400 Subject: [PATCH 088/267] Update goal documentation for multi-arg optional commands (#1214) Update the documentation for passing multiple arguments for goal commands, and standardize the usage styling. --- cmd/goal/account.go | 2 +- cmd/goal/clerk.go | 6 +++--- cmd/goal/multisig.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 78417e5157..75107c9376 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -343,7 +343,7 @@ var deleteCmd = &cobra.Command{ } var newMultisigCmd = &cobra.Command{ - Use: "new [addr1 addr2 ...]", + Use: "new [address 1] [address 2]...", Short: "Create a new multisig account", Long: `Create a new multisig account from a list of existing non-multisig addresses`, Args: cobra.MinimumNArgs(1), diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 97870af933..a8aea32754 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -552,7 +552,7 @@ var rawsendCmd = &cobra.Command{ } var inspectCmd = &cobra.Command{ - Use: "inspect", + Use: "inspect [input file 1] [input file 2]...", Short: "print a transaction file", Long: `Loads a transaction file, attempts to decode the transaction, and displays the decoded information.`, Run: func(cmd *cobra.Command, args []string) { @@ -624,7 +624,7 @@ func getProto(versArg string) (protocol.ConsensusVersion, config.ConsensusParams } var signCmd = &cobra.Command{ - Use: "sign -i INFILE -o OUTFILE", + Use: "sign -i [input file] -o [output file]", Short: "Sign a transaction file", Long: `Sign the passed transaction file, which may contain one or more transactions. If the infile and the outfile are the same, this overwrites the file with the new, signed data.`, Args: validateNoPosArgsFn, @@ -832,7 +832,7 @@ func disassembleFile(fname, outname string) { } var compileCmd = &cobra.Command{ - Use: "compile", + Use: "compile [input file 1] [input file 2]...", Short: "compile a contract program", Long: "Reads a TEAL contract program and compiles it to binary output and contract address.", Run: func(cmd *cobra.Command, args []string) { diff --git a/cmd/goal/multisig.go b/cmd/goal/multisig.go index efd9b22454..3ec5a26d31 100644 --- a/cmd/goal/multisig.go +++ b/cmd/goal/multisig.go @@ -72,7 +72,7 @@ var multisigCmd = &cobra.Command{ } var addSigCmd = &cobra.Command{ - Use: "sign -t TXFILE -a ADDR", + Use: "sign -t [transaction file] -a [address]", Short: "Add a signature to a multisig transaction", Long: `Start a multisig, or add a signature to an existing multisig, for a given transaction.`, Args: validateNoPosArgsFn, @@ -137,7 +137,7 @@ var addSigCmd = &cobra.Command{ } var signProgramCmd = &cobra.Command{ - Use: "signprogram -t TXFILE -a ADDR", + Use: "signprogram -t [transaction file] -a [address]", Short: "Add a signature to a multisig LogicSig", Long: `Start a multisig LogicSig, or add a signature to an existing multisig, for a given program.`, Args: validateNoPosArgsFn, @@ -223,7 +223,7 @@ var signProgramCmd = &cobra.Command{ } var mergeSigCmd = &cobra.Command{ - Use: "merge -o MERGEDTXFILE TXFILE1 TXFILE2 ...", + Use: "merge -o [merged transaction file] [input file 1] [input file 2]...", Short: "Merge multisig signatures on transactions", Long: `Combine multiple partially-signed multisig transactions, and write out transactions with a single merged multisig signature.`, Run: func(cmd *cobra.Command, args []string) { From c9e152e86da839da961151f923310b64216616ec Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 2 Jul 2020 00:05:51 -0400 Subject: [PATCH 089/267] Automatic DNSBootstrapID for betanet (#1219) This PR adds automatic detection of betanet genesis files and update the DNSBootstrapID if it has not been set by the user. The implementation follow the same logic as with devnet, and would allow us to deploy betanet nodes without requiring the end user to provide a config.json file which sets the DNSBootstrapID explicitly. --- config/config.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 8a9084c31e..6161adda10 100644 --- a/config/config.go +++ b/config/config.go @@ -33,6 +33,9 @@ import ( // Devnet identifies the 'development network' use for development and not generally accessible publicly const Devnet protocol.NetworkID = "devnet" +// Betanet identifies the 'beta network' use for early releases of feature to the public prior to releasing these to mainnet/testnet +const Betanet protocol.NetworkID = "betanet" + // Devtestnet identifies the 'development network for tests' use for running tests against development and not generally accessible publicly const Devtestnet protocol.NetworkID = "devtestnet" @@ -412,8 +415,12 @@ func (cfg Local) DNSBootstrap(network protocol.NetworkID) string { // if user hasn't modified the default DNSBootstrapID in the configuration // file and we're targeting a devnet ( via genesis file ), we the // explicit devnet network bootstrap. - if defaultLocal.DNSBootstrapID == cfg.DNSBootstrapID && network == Devnet { - return "devnet.algodev.network" + if defaultLocal.DNSBootstrapID == cfg.DNSBootstrapID { + if network == Devnet { + return "devnet.algodev.network" + } else if network == Betanet { + return "betanet.algodev.network" + } } return strings.Replace(cfg.DNSBootstrapID, "", string(network), -1) } From 366f1c3f219ae117098489ce6a628b678aa26f9f Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 2 Jul 2020 00:06:31 -0400 Subject: [PATCH 090/267] Improve TestConnMonitorStageTiming benchmark performance (#1215) This change reduces the time it takes to execute the TestConnMonitorStageTiming by factor of 14. It's expected to take about 10 seconds on travis, whereas it used to take 150 seconds. --- network/connPerfMon_test.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/network/connPerfMon_test.go b/network/connPerfMon_test.go index 8eceb4c620..25a43fb558 100644 --- a/network/connPerfMon_test.go +++ b/network/connPerfMon_test.go @@ -32,7 +32,8 @@ func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { out = make([]IncomingMessage, 0, N*2) msgIndex := uint64(0) timer := int64(0) - msgPerSecond := uint64(1500) + msgPerSecond := uint64(500) + msgInterval := int64(time.Second) / int64(msgPerSecond) for { if len(out) >= N { break @@ -65,9 +66,9 @@ func makeMsgPool(N int, peers []Peer) (out []IncomingMessage) { msgIndex++ if msgIndex%msgPerSecond == 0 { - timer += int64(time.Second) + timer += int64(time.Second * 3) } - timer += int64(123 * time.Nanosecond) + timer += msgInterval + int64(123*time.Nanosecond) } return } @@ -92,7 +93,7 @@ func BenchmarkConnMonitor(b *testing.B) { func TestConnMonitorStageTiming(t *testing.T) { peers := []Peer{&wsPeer{}, &wsPeer{}, &wsPeer{}, &wsPeer{}} - msgPool := makeMsgPool(1000000, peers) + msgPool := makeMsgPool(60000, peers) stageTimings := make([]time.Duration, 5) stageNotifyCalls := make([]int, 5) From 4f9bb07efb8d8a8cc7df9a2d4e70b50a9295e6cb Mon Sep 17 00:00:00 2001 From: egieseke Date: Wed, 8 Jul 2020 12:09:45 -0400 Subject: [PATCH 091/267] Add support for single go version (#1185) go.mod is now used to specify the go version. Updated go version to 1.12.17 --- README.md | 4 +- docker/Dockerfile | 2 +- docker/build/Dockerfile | 2 +- docker/build/Dockerfile-deploy | 2 +- docker/build/cicd.alpine.Dockerfile | 3 +- docker/build/cicd.centos.Dockerfile | 5 +- docker/build/cicd.ubuntu.Dockerfile | 2 +- docker/build/docker.ubuntu.Dockerfile | 3 +- docker/build/mule.go.centos.Dockerfile | 2 +- docker/build/mule.go.debian.Dockerfile | 3 +- go.mod | 2 +- mule.yaml | 293 ++++++++++-------- scripts/buildhost/linux-arm.sh | 3 +- scripts/configure_dev.sh | 2 +- scripts/deploy_linux_version.sh | 3 +- scripts/get_golang_version.sh | 2 + scripts/get_installed_golang_version.sh | 2 + scripts/release/build/build_algod_docker.sh | 3 +- scripts/release/build/rpm/build.sh | 5 +- scripts/release/common/setup.sh | 5 +- .../release/mule/package/docker/package.sh | 6 +- scripts/travis/build.sh | 5 + scripts/travis/configure_dev.sh | 46 +-- scripts/travis/integration_test.sh | 9 +- scripts/travis/test.sh | 8 + 25 files changed, 213 insertions(+), 209 deletions(-) create mode 100755 scripts/get_golang_version.sh create mode 100755 scripts/get_installed_golang_version.sh diff --git a/README.md b/README.md index 2a4d16535e..8849ca0b96 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ about using and installing the algorand platform. ## Building from source ## -Development is done using the [Go Programming Language](https://golang.org/) -version 1.12.x, and this document assumes that you have a functioning +Development is done using the [Go Programming Language](https://golang.org/). +The version of go is specified in the project's [go.mod](go.mod) file. This document assumes that you have a functioning environment setup. If you need assistance setting up an environment please visit the [official Go documentation website](https://golang.org/doc/). diff --git a/docker/Dockerfile b/docker/Dockerfile index 15d4a70057..71fcc16d68 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:18.04 -ENV GOLANG_VERSION 1.12 +ARG GOLANG_VERSION ENV DEBIAN_FRONTEND noninteractive RUN apt update && apt-get install -y git libboost-all-dev wget sqlite3 autoconf sudo tzdata bsdmainutils diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index 416b9ef0ee..9a1bf07a2e 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:16.04 -ENV GOLANG_VERSION 1.12 +ARG GOLANG_VERSION RUN apt-get update && apt-get install -y git libboost-all-dev wget sqlite3 autoconf build-essential shellcheck WORKDIR /root diff --git a/docker/build/Dockerfile-deploy b/docker/build/Dockerfile-deploy index 18671309b9..63eb7cb5da 100644 --- a/docker/build/Dockerfile-deploy +++ b/docker/build/Dockerfile-deploy @@ -1,5 +1,5 @@ FROM ubuntu:18.04 -ENV GOLANG_VERSION 1.12 +ARG GOLANG_VERSION RUN apt-get update && apt-get install -y git libboost-all-dev wget sqlite3 autoconf jq bsdmainutils shellcheck WORKDIR /root diff --git a/docker/build/cicd.alpine.Dockerfile b/docker/build/cicd.alpine.Dockerfile index 9824eef64e..228a71ee88 100644 --- a/docker/build/cicd.alpine.Dockerfile +++ b/docker/build/cicd.alpine.Dockerfile @@ -1,4 +1,5 @@ -FROM arm32v6/golang:1.12-alpine +ARG GOLANG_VERSION +FROM arm32v6/golang:${GOLANG_VERSION}-alpine RUN apk update && \ apk add make && \ apk add bash && \ diff --git a/docker/build/cicd.centos.Dockerfile b/docker/build/cicd.centos.Dockerfile index 4758116620..591f403e31 100644 --- a/docker/build/cicd.centos.Dockerfile +++ b/docker/build/cicd.centos.Dockerfile @@ -1,15 +1,14 @@ ARG ARCH="amd64" FROM ${ARCH}/centos:7 - +ARG GOLANG_VERSION +ARG ARCH="amd64" RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum update -y && \ yum install -y autoconf wget awscli git gnupg2 nfs-utils python3-devel sqlite3 boost-devel expect jq \ libtool gcc-c++ libstdc++-devel libstdc++-static rpmdevtools createrepo rpm-sign bzip2 which ShellCheck \ libffi-devel openssl-devel WORKDIR /root -ARG ARCH -ENV GOLANG_VERSION 1.12.17 RUN wget https://dl.google.com/go/go${GOLANG_VERSION}.linux-${ARCH%v*}.tar.gz \ && tar -xvf go${GOLANG_VERSION}.linux-${ARCH%v*}.tar.gz && \ mv go /usr/local diff --git a/docker/build/cicd.ubuntu.Dockerfile b/docker/build/cicd.ubuntu.Dockerfile index 53715c9e94..cbc3506baa 100644 --- a/docker/build/cicd.ubuntu.Dockerfile +++ b/docker/build/cicd.ubuntu.Dockerfile @@ -1,7 +1,7 @@ ARG ARCH="amd64" FROM ${ARCH}/ubuntu:18.04 -ENV GOLANG_VERSION 1.12 +ARG GOLANG_VERSION ARG ARCH="amd64" RUN apt-get update && apt-get install -y build-essential git libboost-all-dev wget sqlite3 autoconf jq bsdmainutils shellcheck WORKDIR /root diff --git a/docker/build/docker.ubuntu.Dockerfile b/docker/build/docker.ubuntu.Dockerfile index 3fee7a33c8..ba3dd83c8b 100644 --- a/docker/build/docker.ubuntu.Dockerfile +++ b/docker/build/docker.ubuntu.Dockerfile @@ -1,6 +1,7 @@ ARG ARCH="amd64" FROM ${ARCH}/ubuntu:18.04 +ARG GOLANG_VERSION RUN apt-get update && apt-get install curl python python3.7 python3-pip build-essential apt-transport-https ca-certificates software-properties-common -y && \ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - && \ add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" && \ @@ -13,7 +14,7 @@ RUN update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 && pip3 install mulecli RUN apt-get update && apt-get install -y autoconf bsdmainutils git libboost-all-dev && \ - curl https://dl.google.com/go/go1.12.linux-amd64.tar.gz | tar -xzf - && \ + curl https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz | tar -xzf - && \ mv go /usr/local WORKDIR /root diff --git a/docker/build/mule.go.centos.Dockerfile b/docker/build/mule.go.centos.Dockerfile index 734a388dc5..a827ae3f5a 100644 --- a/docker/build/mule.go.centos.Dockerfile +++ b/docker/build/mule.go.centos.Dockerfile @@ -1,7 +1,7 @@ ARG ARCH="amd64" FROM ${ARCH}/centos:7 -ENV GOLANG_VERSION 1.12 +ARG GOLANG_VERSION ARG ARCH="amd64" RUN yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \ yum update -y && \ diff --git a/docker/build/mule.go.debian.Dockerfile b/docker/build/mule.go.debian.Dockerfile index 846bb28667..fb9ae896d9 100644 --- a/docker/build/mule.go.debian.Dockerfile +++ b/docker/build/mule.go.debian.Dockerfile @@ -1,11 +1,12 @@ FROM docker:19 as docker FROM python:3.7 +ARG GOLANG_VERSION COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker COPY *.yaml /root/ RUN apt-get update && apt-get install -y autoconf bsdmainutils build-essential curl git libboost-all-dev && \ - curl https://dl.google.com/go/go1.12.linux-amd64.tar.gz | tar -xzf - && \ + curl https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz | tar -xzf - && \ mv go /usr/local && \ pip install mulecli diff --git a/go.mod b/go.mod index 95e8290a76..e5445e92d6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/algorand/go-algorand -go 1.12 +go 1.12.17 require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d diff --git a/mule.yaml b/mule.yaml index c30fd5ca51..bfa4b56939 100644 --- a/mule.yaml +++ b/mule.yaml @@ -1,45 +1,131 @@ tasks: + + - task: shell.Shell + name: go-version + command: scripts/get_golang_version.sh + saveLogs: true + + # Stash tasks + - task: stash.Stash + name: linux-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-amd64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: darwin-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/darwin-amd64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: linux-arm64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm64 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + - task: stash.Stash + name: linux-arm32 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm32 + globSpecs: + - tmp/node_pkgs/** + - crypto/libs/** + - gen/devnet/genesis.json + - gen/testnet/genesis.json + - gen/mainnet/genesis.json + + # Unstash tasks + - task: stash.Unstash + name: linux-arm64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm64 + - task: stash.Unstash + name: linux-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-amd64 + - task: stash.Unstash + name: darwin-amd64 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/darwin-amd64 + - task: stash.Unstash + name: linux-arm32 + bucketName: go-algorand-ci-cache + stashId: ${JENKINS_JOB_CACHE_ID}/linux-arm32 + + # Docker tasks - task: docker.Version configFilePath: scripts/configure_dev-deps.sh - task: shell.docker.Ensure + name: centos image: algorand/go-algorand-ci-linux - version: '{{ docker.Version.outputs.version }}' - dockerFilePath: docker/build/cicd.Dockerfile - dependencies: docker.Version + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' + dockerFilePath: docker/build/cicd.centos.Dockerfile + dependencies: shell.Shell.go-version docker.Version + - task: shell.docker.Ensure + name: alpine + image: algorand/go-algorand-ci-linux + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' + dockerFilePath: docker/build/cicd.alpine.Dockerfile + dependencies: shell.Shell.go-version docker.Version - task: docker.Make name: build docker: image: algorand/go-algorand-ci-linux - version: '{{ docker.Version.outputs.version }}' + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' workDir: /go/src/github.com/algorand/go-algorand target: ci-build + dependencies: shell.Shell.go-version docker.Version - task: docker.Make name: fulltest docker: image: algorand/go-algorand-ci-linux - version: '{{ docker.Version.outputs.version }}' + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' workDir: /go/src/github.com/algorand/go-algorand target: fulltest -j4 + dependencies: shell.Shell.go-version docker.Version - task: docker.Make - name: shorttest + name: integration-test docker: + env: + - SHORTTEST=-short image: algorand/go-algorand-ci-linux - version: '{{ docker.Version.outputs.version }}' + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' workDir: /go/src/github.com/algorand/go-algorand - target: shorttest -j4 + target: ci-integration -j4 + dependencies: shell.Shell.go-version docker.Version - task: docker.Make - name: integration-test + name: archive docker: image: algorand/go-algorand-ci-linux - version: '{{ docker.Version.outputs.version }}' + version: '{{ docker.Version.outputs.version }}-{{ shell.Shell.go-version.outputs.stdout }}' + goVersion: '{{ shell.Shell.go-version.outputs.stdout }}' workDir: /go/src/github.com/algorand/go-algorand - target: ci-integration -j4 - - task: shell.Make - name: deps - target: ci-deps + target: ci-archive + dependencies: shell.Shell.go-version docker.Version + + # Local Tasks - task: shell.Make - name: build + name: ci-deps build target: ci-build + - task: shell.Make name: fulltest target: fulltest -j4 @@ -54,146 +140,81 @@ tasks: target: archive jobs: - # Linux arm64 jobs - build-linux-arm64: - configs: - arch: arm64v8 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} - tasks: - - shell.docker.Ensure - - docker.Make.build - test-linux-arm64-fulltest: - configs: - arch: arm64v8 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} - tasks: - - shell.docker.Ensure - - docker.Make.fulltest - test-linux-arm64-shorttest: - configs: - arch: arm64v8 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} - tasks: - - shell.docker.Ensure - - docker.Make.shorttest - test-linux-arm64-integration: - configs: - arch: arm64v8 - docker: - env: - - SHORTTEST=${SHORTTEST} - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} - tasks: - - shell.docker.Ensure - - docker.Make.integration-test - - - # Linux amd64 jobs build-linux-amd64: configs: arch: amd64 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} tasks: - - shell.docker.Ensure + - shell.docker.Ensure.centos - docker.Make.build - test-linux-amd64-fulltest: - configs: - arch: amd64 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} - tasks: - - shell.docker.Ensure - - docker.Make.fulltest - test-linux-amd64-shorttest: +# - stash.Stash.linux-amd64 + test-linux-amd64-integration: configs: arch: amd64 - docker: - env: - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} tasks: - - shell.docker.Ensure - - docker.Make.shorttest - test-linux-amd64-integration: + - shell.docker.Ensure.centos +# - stash.Unstash.linux-amd64 + - docker.Make.integration-test + test-linux-amd64-fulltest: configs: arch: amd64 - docker: - env: - - SHORTTEST=${SHORTTEST} - - TRAVIS_OS_NAME=${TRAVIS_OS_NAME} - - TRAVIS_BRANCH=${TRAVIS_BRANCH} - - TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST} - - S3_RELEASE_BUCKET=${S3_RELEASE_BUCKET} tasks: - - shell.docker.Ensure - - docker.Make.integration-test - + - shell.docker.Ensure.centos + - docker.Make.fulltest + # Darwin amd64 jobs + # build-darwin-amd64: + # configs: + # arch: amd64 + # tasks: + # - shell.Make.build + # - stash.Stash.darwin-amd64 + # test-darwin-amd64-integration: + # configs: + # arch: amd64 + # tasks: + # - stash.Unstash.darwin-amd64 + # - shell.Make.integration-test + # test-darwin-amd64-fulltest: + # configs: + # arch: amd64 + # tasks: + # - shell.Make.fulltest - # Local jobs - build-local: - tasks: - - shell.Make.deps - - shell.Make.build - test-local-fulltest: - tasks: - - shell.Make.deps - - shell.Make.fulltest - test-local-shorttest: - tasks: - - shell.Make.deps - - shell.Make.shorttest - test-local-integration: + # Linux arm64 jobs + build-linux-arm64: + configs: + arch: arm64v8 tasks: - - shell.Make.deps - - shell.Make.integration-test - archive-local: + - shell.docker.Ensure.centos + - docker.Make.build +# - stash.Stash.linux-arm64 + test-linux-arm64-integration: + configs: + arch: arm64v8 tasks: - - shell.Make.deps - - shell.Make.archive - - + - shell.docker.Ensure.centos + - stash.Unstash.linux-arm64 + - docker.Make.integration-test - # Linux arm jobs - build-linux-arm: + # Linux arm32 jobs + build-linux-arm32: configs: arch: arm32v6 - target: ci-build tasks: - - docker.Make - test-linux-arm-shorttest: + - shell.docker.Ensure.alpine + - docker.Make.build + - stash.Stash.linux-arm32 + + # Archive jobs + archive-linux-amd64: configs: - arch: arm32v6 - target: shorttest + arch: amd64 tasks: - - docker.Make + - shell.docker.Ensure.centos + - stash.Unstash.linux-amd64 + # - stash.Unstash.darwin-amd64 + - stash.Unstash.linux-arm64 + - stash.Unstash.linux-arm32 + - docker.Make.archive + diff --git a/scripts/buildhost/linux-arm.sh b/scripts/buildhost/linux-arm.sh index d65770f419..f7b0931559 100755 --- a/scripts/buildhost/linux-arm.sh +++ b/scripts/buildhost/linux-arm.sh @@ -19,7 +19,8 @@ cd ${TMPPATH} AWS_REGION="us-west-2" # this is the private AMI that contains the RasPI VM running on port 5022 -AWS_LINUX_AMI="ami-06819013739d79715" +#AWS_LINUX_AMI="ami-06819013739d79715" +AWS_LINUX_AMI="ami-009f7a201c82c42e1" AWS_INSTANCE_TYPE="i3.xlarge" INSTANCE_NUMBER=$RANDOM diff --git a/scripts/configure_dev.sh b/scripts/configure_dev.sh index cfb8757717..791095ea93 100755 --- a/scripts/configure_dev.sh +++ b/scripts/configure_dev.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -e +set -ex HELP="Usage: $0 [-s] Installs host level dependencies necessary to build go-algorand. diff --git a/scripts/deploy_linux_version.sh b/scripts/deploy_linux_version.sh index 927e624d86..aa023285f8 100755 --- a/scripts/deploy_linux_version.sh +++ b/scripts/deploy_linux_version.sh @@ -77,5 +77,6 @@ echo export NETWORK=${NETWORK} >> ${TMPDIR}/deploy_linux_version_exec.sh echo scripts/deploy_private_version.sh -c \"${CHANNEL}\" -g \"${DEFAULTNETWORK}\" -n \"${NETWORK}\" -f \"${GENESISFILE}\" -b \"${S3_RELEASE_BUCKET}\" >> ${TMPDIR}/deploy_linux_version_exec.sh chmod +x ${TMPDIR}/deploy_linux_version_exec.sh +GOLANG_VERSION=$(./scripts/get_golang_version.sh) sed "s|TMPDIR|${SUBDIR}|g" ${SRCPATH}/docker/build/Dockerfile-deploy > ${TMPDIR}/Dockerfile-deploy -docker build -f ${TMPDIR}/Dockerfile-deploy -t algorand-deploy . +docker build -f ${TMPDIR}/Dockerfile-deploy --build-arg GOLANG_VERSION=${GOLANG_VERSION} -t algorand-deploy . diff --git a/scripts/get_golang_version.sh b/scripts/get_golang_version.sh new file mode 100755 index 0000000000..a5c2afc5e4 --- /dev/null +++ b/scripts/get_golang_version.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +awk '/^go[ \t]+[0-9]+\.[0-9]+(\.[0-9]+)?[ \t]*$/{print $2}' go.mod diff --git a/scripts/get_installed_golang_version.sh b/scripts/get_installed_golang_version.sh new file mode 100755 index 0000000000..f7ccf20f15 --- /dev/null +++ b/scripts/get_installed_golang_version.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +go version 2>&1 | awk 'BEGIN {v=""}; /^go version go[0-9]+\.[0-9]+(\.[0-9]+)?[ \t]*.*$/{v=$3}; END {print(v)}' | sed 's/go//' diff --git a/scripts/release/build/build_algod_docker.sh b/scripts/release/build/build_algod_docker.sh index d836c7b111..309dffc036 100755 --- a/scripts/release/build/build_algod_docker.sh +++ b/scripts/release/build/build_algod_docker.sh @@ -38,10 +38,11 @@ DOCKER_IMAGE="algorand/algod_${CHANNEL_VERSION}:${DOCKER_TAG}" 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" +GOLANG_VERSION=$(script/get_golang_version.sh) echo "building '${DOCKERFILE}' with install file $ALGOD_INSTALL_TAR_FILE" cp "${ALGOD_INSTALL_TAR_FILE}" "./${INPUT_ALGOD_TAR_FILE}" -docker build --build-arg ALGOD_INSTALL_TAR_FILE=${INPUT_ALGOD_TAR_FILE} . -t ${DOCKER_IMAGE} -f ${DOCKERFILE} +docker build --build-arg ALGOD_INSTALL_TAR_FILE=${INPUT_ALGOD_TAR_FILE} --build-arg GOLANG_VERSION=${GOLANG_VERSION} . -t ${DOCKER_IMAGE} -f ${DOCKERFILE} #echo "pushing '${DOCKER_IMAGE}'" #docker push ${DOCKER_IMAGE} diff --git a/scripts/release/build/rpm/build.sh b/scripts/release/build/rpm/build.sh index c84efbee0b..b421ed152b 100755 --- a/scripts/release/build/rpm/build.sh +++ b/scripts/release/build/rpm/build.sh @@ -19,9 +19,10 @@ then exit 1 fi -# Get golang 1.12.9 and build its own copy of go-algorand. +# Install go version specified in go.mod and build its own copy of go-algorand. +GOLANG_VERSION=$(./scripts/get_golang_version.sh) cd "${HOME}" -if ! curl -O https://dl.google.com/go/go1.12.9.linux-amd64.tar.gz +if ! curl -O https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz then echo Golang could not be installed! exit 1 diff --git a/scripts/release/common/setup.sh b/scripts/release/common/setup.sh index 53005f6da3..e4a41e59af 100755 --- a/scripts/release/common/setup.sh +++ b/scripts/release/common/setup.sh @@ -49,9 +49,10 @@ COMMIT_HASH=$(git rev-parse "${BRANCH}") export DEBIAN_FRONTEND=noninteractive -# Install latest go.1.12.9. +# Install go version specified in go.mod +GOLANG_VERSION=$(./scripts/get_golang_version.sh) cd "${HOME}" -if ! curl -O https://dl.google.com/go/go1.12.9.linux-amd64.tar.gz +if ! curl -O https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz then echo Golang could not be installed! exit 1 diff --git a/scripts/release/mule/package/docker/package.sh b/scripts/release/mule/package/docker/package.sh index ce5138068c..ec20292a1f 100755 --- a/scripts/release/mule/package/docker/package.sh +++ b/scripts/release/mule/package/docker/package.sh @@ -38,18 +38,18 @@ DOCKERFILE="./docker/build/algod.Dockerfile" START_ALGOD_FILE="./docker/release/start_algod_docker.sh" ALGOD_DOCKER_INIT="./docker/release/algod_docker_init.sh" -GO_VERSION=1.12 +GOLANG_VERSION=$(./scripts/get_golang_version.sh) GOROOT=/usr/local/go GOPATH="$HOME/go" PATH="$GOPATH/bin:$GOROOT/bin:$PATH" -curl "https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz" | tar -xzf - +curl "https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz" | tar -xzf - mv go /usr/local echo "building '$DOCKERFILE' with install file $ALGOD_INSTALL_TAR_FILE" cp "$ALGOD_INSTALL_TAR_FILE" "/tmp/$INPUT_ALGOD_TAR_FILE" cp "$ALGOD_DOCKER_INIT" /tmp -docker build --build-arg ALGOD_INSTALL_TAR_FILE="$INPUT_ALGOD_TAR_FILE" /tmp -t "$DOCKER_IMAGE" -f "$DOCKERFILE" +docker build --build-arg ALGOD_INSTALL_TAR_FILE="$INPUT_ALGOD_TAR_FILE" --build-arg GOLANG_VERSION="${GOLANG_VERSION}" /tmp -t "$DOCKER_IMAGE" -f "$DOCKERFILE" mkdir -p "/tmp/$NEW_PKG_DIR" diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index ddf54989cb..5399269113 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -26,6 +26,7 @@ done # turn off exit on error set +e +set -x CONFIGURE_SUCCESS=false @@ -34,6 +35,10 @@ SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" OS=$("${SCRIPTPATH}/../ostype.sh") ARCH=$("${SCRIPTPATH}/../archtype.sh") +curl -sL -o ~/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme +chmod +x ~/gimme +eval $(~/gimme $("${SCRIPTPATH}/../get_golang_version.sh")) + # travis sometimes fail to download a dependency. trying multiple times might help. for (( attempt=1; attempt<=5; attempt++ )) do diff --git a/scripts/travis/configure_dev.sh b/scripts/travis/configure_dev.sh index 1cf0ebbbb7..f5d4e7c057 100755 --- a/scripts/travis/configure_dev.sh +++ b/scripts/travis/configure_dev.sh @@ -4,59 +4,21 @@ set +e SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" - OS=$("${SCRIPTPATH}/../ostype.sh") ARCH=$("${SCRIPTPATH}/../archtype.sh") -if [ "${OS}" = "linux" ]; then - if [[ "${ARCH}" = "arm64" ]]; then - go version 2>/dev/null - if [ "$?" != "0" ]; then - echo "Go cannot be found; downloading..." - # go is not installed ? - wget -q https://dl.google.com/go/go1.12.9.linux-arm64.tar.gz - if [ "$?" = "0" ]; then - set -e - sudo tar -C /usr/local -xzf ./go1.12.9.linux-arm64.tar.gz - rm -f ./go1.12.9.linux-arm64.tar.gz - sudo ln -s /usr/local/go/bin/go /usr/local/bin/go - sudo ln -s /usr/local/go/bin/godoc /usr/local/bin/godoc - sudo ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt - go version - else - echo "Failed to download go" - exit 1 - fi - fi +if [[ "${OS}" == "linux" ]]; then + if [[ "${ARCH}" == "arm64" ]]; then set -e sudo apt-get update -y sudo apt-get -y install sqlite3 python3-venv libffi-dev libssl-dev - fi - if [[ "${ARCH}" = "arm" ]]; then + elif [[ "${ARCH}" == "arm" ]]; then sudo sh -c 'echo "CONF_SWAPSIZE=1024" > /etc/dphys-swapfile; dphys-swapfile setup; dphys-swapfile swapon' - go version 2>/dev/null - if [ "$?" != "0" ]; then - echo "Go cannot be found; downloading..." - # go is not installed ? - wget -q https://dl.google.com/go/go1.12.9.linux-armv6l.tar.gz - if [ "$?" = "0" ]; then - set -e - sudo tar -C /usr/local -xzf ./go1.12.9.linux-armv6l.tar.gz - rm -f ./go1.12.9.linux-armv6l.tar.gz - sudo ln -s /usr/local/go/bin/go /usr/local/bin/go - sudo ln -s /usr/local/go/bin/godoc /usr/local/bin/godoc - sudo ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt - go version - else - echo "Failed to download go" - exit 1 - fi - fi set -e sudo apt-get update -y sudo apt-get -y install sqlite3 fi -elif [ "${OS}" = "darwin" ]; then +elif [[ "${OS}" == "darwin" ]]; then # we don't want to upgrade boost if we already have it, as it will try to update # other components. brew update diff --git a/scripts/travis/integration_test.sh b/scripts/travis/integration_test.sh index c5b8655c60..6a73011521 100755 --- a/scripts/travis/integration_test.sh +++ b/scripts/travis/integration_test.sh @@ -10,18 +10,15 @@ set -e SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" + export BUILD_TYPE="integration" +"${SCRIPTPATH}/build.sh" --make_debug + if [ "${USER}" = "travis" ]; then # we're running on a travis machine - "${SCRIPTPATH}/build.sh" --make_debug - GOPATHBIN=$(go env GOPATH)/bin - export PATH=$PATH:$GOPATHBIN "${SCRIPTPATH}/travis_wait.sh" 120 "${SCRIPTPATH}/test.sh" else # we're running on an ephermal build machine - "${SCRIPTPATH}/build.sh" --make_debug - GOPATHBIN=$(go env GOPATH)/bin - export PATH=$PATH:$GOPATHBIN "${SCRIPTPATH}/test.sh" fi diff --git a/scripts/travis/test.sh b/scripts/travis/test.sh index c23bd4bd96..ed1a898b38 100755 --- a/scripts/travis/test.sh +++ b/scripts/travis/test.sh @@ -1,16 +1,24 @@ #!/usr/bin/env bash set -e +set -x SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" OS=$("${SCRIPTPATH}/../ostype.sh") ARCH=$("${SCRIPTPATH}/../archtype.sh") +curl -sL -o ~/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme +chmod +x ~/gimme +eval $(~/gimme $("${SCRIPTPATH}/../get_golang_version.sh")) + if [ "${OS}-${ARCH}" = "linux-arm" ]; then # for arm, no tests need to be invoked. exit 0 fi +GOPATHBIN=$(go env GOPATH)/bin +export PATH=$PATH:$GOPATHBIN + make fixcheck scripts/travis/run_tests.sh; scripts/travis/after_build.sh; From 7cca251f12f2e9901b12b8b255c245bbe38f12a0 Mon Sep 17 00:00:00 2001 From: btoll Date: Thu, 9 Jul 2020 16:17:18 -0400 Subject: [PATCH 092/267] Test build packages on multiple architectures (#1229) This makes sure that the test build scripts work on arm* architectures. The main change is to test the ARCH variable for some variation of arm* in mule/test/test.sh and will call the run_tests script directly. This bypasses the path that amd64 builds take, which is to further spin up more docker containers and run the run_tests in each of them. I feel this is unnecessary for arm*, and further the run_tests script initiates all the tests that all architectures must run. The rest (and the majority) of the changes were necessary to allow the run_tests shell script to be called directly. These changes now have each shell script determine the values which it needs rather than having them passed from a Makefile. This is better practice and is inline with what is being done in the indexer project, as well. --- Makefile | 3 +- package-deploy.yaml | 4 +- package-sign.yaml | 8 ++-- package-test.yaml | 4 +- package.yaml | 6 +-- scripts/release/mule/Makefile.mule | 29 ++++++------ scripts/release/mule/deploy/deb/deploy.sh | 21 +++------ scripts/release/mule/deploy/rpm/deploy.sh | 2 +- scripts/release/mule/package/deb/package.sh | 47 +++++-------------- .../release/mule/package/docker/package.sh | 25 ++-------- scripts/release/mule/package/rpm/package.sh | 27 ++++------- scripts/release/mule/sign/sign.sh | 23 +++------ scripts/release/mule/test/test.sh | 31 ++++++------ scripts/release/mule/test/tests/goal.sh | 2 +- scripts/release/mule/test/tests/run_tests | 2 +- .../mule/test/tests/verify_pkg_string.sh | 4 +- 16 files changed, 89 insertions(+), 149 deletions(-) diff --git a/Makefile b/Makefile index a0d7907c1e..7939df229c 100644 --- a/Makefile +++ b/Makefile @@ -293,5 +293,6 @@ SUPPORTED_ARCHIVE_OS_ARCH = linux/amd64 linux/arm64 linux/arm darwin/amd64 archive: CHANNEL=$(CHANNEL) \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$${PATH} \ + PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/bin:$${PATH} \ scripts/upload_version.sh $(CHANNEL) $(SRCPATH)/tmp/node_pkgs $(S3_RELEASE_BUCKET) + diff --git a/package-deploy.yaml b/package-deploy.yaml index 53ee240aac..a243dfa9f2 100644 --- a/package-deploy.yaml +++ b/package-deploy.yaml @@ -31,7 +31,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-deploy-deb WORKDIR=/projects/go-algorand + target: mule-deploy-deb - task: docker.Make name: rpm @@ -48,7 +48,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-deploy-rpm WORKDIR=/projects/go-algorand + target: mule-deploy-rpm - task: s3.BucketCopy name: deb diff --git a/package-sign.yaml b/package-sign.yaml index da8f78bc3a..70e5b04d80 100644 --- a/package-sign.yaml +++ b/package-sign.yaml @@ -23,7 +23,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-sign-deb WORKDIR=/projects/go-algorand + target: mule-sign-deb - task: docker.Make name: package-sign-rpm @@ -39,7 +39,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-sign-rpm WORKDIR=/projects/go-algorand + target: mule-sign-rpm - task: docker.Make name: package-sign-tarball @@ -55,7 +55,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-sign-tar.gz WORKDIR=/projects/go-algorand + target: mule-sign-tar.gz - task: docker.Make name: package-sign-source @@ -67,7 +67,7 @@ tasks: $XDG_RUNTIME_DIR/gnupg/S.gpg-agent:/root/.gnupg/S.gpg-agent, $HOME/.gnupg/pubring.kbx:/root/.gnupg/pubring.kbx ] - target: mule-sign-source WORKDIR=/projects/go-algorand + target: mule-sign-source - task: s3.DownloadFile name: deb diff --git a/package-test.yaml b/package-test.yaml index 323a33ee00..f2718ab376 100644 --- a/package-test.yaml +++ b/package-test.yaml @@ -20,7 +20,7 @@ tasks: AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY ] volumes: [ /var/run/docker.sock:/var/run/docker.sock ] - target: mule-test-deb WORKDIR=/projects/go-algorand + target: mule-test-deb - task: docker.Make name: package-test-rpm @@ -33,7 +33,7 @@ tasks: AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY ] volumes: [ /var/run/docker.sock:/var/run/docker.sock ] - target: mule-test-rpm WORKDIR=/projects/go-algorand + target: mule-test-rpm - task: s3.DownloadFile name: deb diff --git a/package.yaml b/package.yaml index ad818f0461..2ac7dffbb9 100644 --- a/package.yaml +++ b/package.yaml @@ -37,7 +37,7 @@ tasks: image: algorand/go-algorand-ci-linux-centos version: '{{ docker.Version.outputs.version }}' workDir: /projects/go-algorand - target: mule-package-rpm WORKDIR=/projects/go-algorand + target: mule-package-rpm - task: docker.Make name: deb @@ -45,7 +45,7 @@ tasks: image: algorand/go-algorand-ci-linux-ubuntu version: '{{ docker.Version.outputs.version }}' workDir: /projects/go-algorand - target: mule-package-deb WORKDIR=/projects/go-algorand + target: mule-package-deb - task: docker.Make name: docker-image @@ -54,7 +54,7 @@ tasks: version: '{{ docker.Version.outputs.version }}' workDir: /projects/go-algorand volumes: [ /var/run/docker.sock:/var/run/docker.sock ] - target: mule-package-docker WORKDIR=/projects/go-algorand + target: mule-package-docker jobs: package: diff --git a/scripts/release/mule/Makefile.mule b/scripts/release/mule/Makefile.mule index ce94e22aea..ca34b36878 100644 --- a/scripts/release/mule/Makefile.mule +++ b/scripts/release/mule/Makefile.mule @@ -1,9 +1,8 @@ # This file is imported into go-algorand/Makefile. -ARCH_BIT = $(shell uname -m) -VER = $(shell ./scripts/compute_build_number.sh -f) +PKG_DIR = $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) -.PHONY: ci-clean ci-deps ci-setup ci-build package package-deb package-rpm package-$(OS_TYPE)-$(ARCH) package-deb-$(OS_TYPE)-$(ARCH) package-rpm-$(OS_TYPE)-$(ARCH) +.PHONY: ci-clean ci-deps ci-setup ci-build ci-clean: rm -rf tmp @@ -13,7 +12,7 @@ ci-deps: scripts/check_deps.sh ci-setup: - mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) + mkdir -p $(PKG_DIR) ci-test: ifeq ($(ARCH), amd64) @@ -26,18 +25,18 @@ endif done ci-integration: - NODEBINDIR=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/bin:$$PATH \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/tools:$$PATH \ - PATH=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/test-utils:$$PATH \ + NODEBINDIR=/bin \ + PATH=$(PKG_DIR)/bin:$$PATH \ + PATH=$(PKG_DIR)/tools:$$PATH \ + PATH=$(PKG_DIR)/test-utils:$$PATH \ SRCROOT=$(SRCPATH) \ test/scripts/e2e.sh -c $(CHANNEL) -n ci-build: buildsrc gen ci-setup - CHANNEL=$(CHANNEL) PKG_ROOT=$(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH) NO_BUILD=True VARIATIONS=$(OS_TYPE)-$(ARCH) \ + CHANNEL=$(CHANNEL) PKG_ROOT=$(PKG_DIR) NO_BUILD=True VARIATIONS=$(OS_TYPE)-$(ARCH) \ scripts/build_packages.sh $(OS_TYPE)/$(ARCH) && \ - mkdir -p $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/data && \ - cp gen/devnet/genesis.json $(SRCPATH)/tmp/node_pkgs/$(OS_TYPE)/$(ARCH)/$(CHANNEL)/$(OS_TYPE)-$(ARCH)/data + mkdir -p $(PKG_DIR)/data && \ + cp gen/devnet/genesis.json $(PKG_DIR)/data # Builds targets from the sub-directories of ./scripts/release/mule/, such as `mule-package`, `mule-sign`, `mule-test`. # https://scene-si.org/2019/12/04/make-dynamic-makefile-targets/ @@ -45,17 +44,17 @@ mule = $(shell ls -d scripts/release/mule/*/ | awk 'BEGIN { FS="/" ; OFS="-" } { mule-deploy-%: PKG_TYPE=$* mule-deploy-%: - scripts/release/mule/deploy/$(PKG_TYPE)/deploy.sh $(OS_TYPE) $(ARCH) $(ARCH_BIT) $(VER) $$WORKDIR + scripts/release/mule/deploy/$(PKG_TYPE)/deploy.sh mule-package-%: PKG_TYPE=$* mule-package-%: - scripts/release/mule/package/$(PKG_TYPE)/package.sh $(OS_TYPE) $(ARCH) $$WORKDIR + scripts/release/mule/package/$(PKG_TYPE)/package.sh mule-sign-%: PKG_TYPE=$* mule-sign-%: - scripts/release/mule/sign/sign.sh $(OS_TYPE) $(ARCH) $(ARCH_BIT) $(VER) $(PKG_TYPE) $$WORKDIR + scripts/release/mule/sign/sign.sh $(PKG_TYPE) mule-test-%: PKG_TYPE=$* mule-test-%: - scripts/release/mule/test/test.sh $(OS_TYPE) $(ARCH) $(ARCH_BIT) $(VER) $(PKG_TYPE) $$WORKDIR + scripts/release/mule/test/test.sh $(PKG_TYPE) diff --git a/scripts/release/mule/deploy/deb/deploy.sh b/scripts/release/mule/deploy/deb/deploy.sh index c01eb7138e..83b044ae54 100755 --- a/scripts/release/mule/deploy/deb/deploy.sh +++ b/scripts/release/mule/deploy/deb/deploy.sh @@ -2,26 +2,17 @@ set -ex -WORKDIR="$5" - -if [ -z "$WORKDIR" ] -then - echo "WORKDIR variable must be defined." - exit 1 -fi - echo date "+build_release begin SNAPSHOT stage %Y%m%d_%H%M%S" echo -OS_TYPE="$1" -ARCH_TYPE="$2" -ARCH_BIT="$3" -VERSION=${VERSION:-$4} - +ARCH_BIT=$(uname -m) +ARCH_TYPE=$(./scripts/archtype.sh) +OS_TYPE=$(./scripts/ostype.sh) +VERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} BRANCH=${BRANCH:-$(git rev-parse --abbrev-ref HEAD)} -CHANNEL=${CHANNEL:-$("$WORKDIR/scripts/compute_branch_channel.sh" "$BRANCH")} -PKG_DIR="$WORKDIR/tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE" +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} +PKG_DIR="./tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE" SIGNING_KEY_ADDR=dev@algorand.com chmod 400 "$HOME/.gnupg" diff --git a/scripts/release/mule/deploy/rpm/deploy.sh b/scripts/release/mule/deploy/rpm/deploy.sh index 35a0abb281..d8903f2df2 100755 --- a/scripts/release/mule/deploy/rpm/deploy.sh +++ b/scripts/release/mule/deploy/rpm/deploy.sh @@ -3,7 +3,7 @@ set -ex -VERSION=$(./scripts/compute_build_number.sh -f) +VERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} mule -f package-deploy.yaml package-deploy-setup-gnupg diff --git a/scripts/release/mule/package/deb/package.sh b/scripts/release/mule/package/deb/package.sh index 83790d12b4..41e1e43221 100755 --- a/scripts/release/mule/package/deb/package.sh +++ b/scripts/release/mule/package/deb/package.sh @@ -7,30 +7,21 @@ echo date "+build_release begin PACKAGE DEB stage %Y%m%d_%H%M%S" echo -OS_TYPE="$1" -ARCH="$2" -WORKDIR="$3" - -if [ -z "$OS_TYPE" ] || [ -z "$ARCH" ] || [ -z "$WORKDIR" ]; then - echo OS, ARCH and WORKDIR variables must be defined. - exit 1 -fi - -BRANCH=${BRANCH:-$("$WORKDIR/scripts/compute_branch.sh" "$BRANCH")} -CHANNEL=${CHANNEL:-$("$WORKDIR/scripts/compute_branch_channel.sh" "$BRANCH")} -BASE="$WORKDIR/tmp/node_pkgs/$OS_TYPE/$ARCH" -mkdir -p "$BASE/$CHANNEL/$OS_TYPE-$ARCH/bin" -ALGO_BIN="$BASE/$CHANNEL/$OS_TYPE-$ARCH/bin" -OUTDIR="$BASE" -PKG_NAME=$("$WORKDIR/scripts/compute_package_name.sh" "${CHANNEL:-stable}") -VER=${VERSION:-$("$WORKDIR/scripts/compute_build_number.sh" -f)} +ARCH=$(./scripts/archtype.sh) +OS_TYPE=$(./scripts/ostype.sh) +BRANCH=${BRANCH:-$(./scripts/compute_branch.sh "$BRANCH")} +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} +OUTDIR="./tmp/node_pkgs/$OS_TYPE/$ARCH" +mkdir -p "$OUTDIR/bin" +ALGO_BIN="./tmp/node_pkgs/$OS_TYPE/$ARCH/$CHANNEL/$OS_TYPE-$ARCH/bin" +PKG_NAME=$(./scripts/compute_package_name.sh "${CHANNEL:-stable}") +VER=${VERSION:-$(./scripts/compute_build_number.sh -f)} echo "Building debian package for '${OS} - ${ARCH}'" -if [ "${DEFAULTNETWORK}" = "" ]; then - DEFAULTNETWORK=$("$WORKDIR/scripts/compute_branch_network.sh") -fi -DEFAULT_RELEASE_NETWORK=$("$WORKDIR/scripts/compute_branch_release_network.sh" "${DEFAULTNETWORK}") +DEFAULTNETWORK=$("./scripts/compute_branch_network.sh") +DEFAULT_RELEASE_NETWORK=$("./scripts/compute_branch_release_network.sh" "${DEFAULTNETWORK}") +export DEFAULT_RELEASE_NETWORK PKG_ROOT=$(mktemp -d) trap "rm -rf $PKG_ROOT" 0 @@ -58,19 +49,7 @@ for data in "${data_files[@]}"; do cp "installer/${data}" "${PKG_ROOT}/var/lib/algorand" done -if [ ! -z "${RELEASE_GENESIS_PROCESS}" ]; then - genesis_dirs=("devnet" "testnet" "mainnet" "betanet") - for dir in "${genesis_dirs[@]}"; do - mkdir -p "${PKG_ROOT}/var/lib/algorand/genesis/${dir}" - cp "${WORKDIR}/installer/genesis/${dir}/genesis.json" "${PKG_ROOT}/var/lib/algorand/genesis/${dir}/genesis.json" - done - # Copy the appropriate network genesis.json for our default (in root ./genesis folder) - cp "${PKG_ROOT}/var/lib/algorand/genesis/${DEFAULT_RELEASE_NETWORK}/genesis.json" "${PKG_ROOT}/var/lib/algorand" -elif [[ "${CHANNEL}" == "dev" || "${CHANNEL}" == "stable" || "${CHANNEL}" == "nightly" || "${CHANNEL}" == "beta" ]]; then - cp "${WORKDIR}/installer/genesis/${DEFAULTNETWORK}/genesis.json" "${PKG_ROOT}/var/lib/algorand/genesis.json" -else - cp "${WORKDIR}/installer/genesis/${DEFAULTNETWORK}/genesis.json" "${PKG_ROOT}/var/lib/algorand" -fi +cp "./installer/genesis/${DEFAULTNETWORK}/genesis.json" "${PKG_ROOT}/var/lib/algorand/genesis.json" systemd_files=("algorand.service" "algorand@.service") mkdir -p "${PKG_ROOT}/lib/systemd/system" diff --git a/scripts/release/mule/package/docker/package.sh b/scripts/release/mule/package/docker/package.sh index ec20292a1f..b9e8a65878 100755 --- a/scripts/release/mule/package/docker/package.sh +++ b/scripts/release/mule/package/docker/package.sh @@ -6,19 +6,12 @@ echo date "+build_release begin PACKAGE DOCKER stage %Y%m%d_%H%M%S" echo -OS_TYPE="$1" -ARCH="$2" -WORKDIR="$3" - -if [ -z "$OS_TYPE" ] || [ -z "$ARCH" ] || [ -z "$WORKDIR" ]; then - echo "OS=$OS, ARCH=$ARCH and WORKDIR=$WORKDIR variables must be defined." - exit 1 -fi - -BRANCH=$(./scripts/compute_branch.sh) -CHANNEL=$(./scripts/compute_branch_channel.sh "$BRANCH") +ARCH=$(./scripts/archtype.sh) +OS_TYPE=$(./scripts/ostype.sh) +BRANCH=${BRANCH:-$(./scripts/compute_branch.sh "$BRANCH")} +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} PKG_ROOT_DIR="./tmp/node_pkgs/$OS_TYPE/$ARCH" -FULLVERSION=$(./scripts/compute_build_number.sh -f) +FULLVERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} ALGOD_INSTALL_TAR_FILE="$PKG_ROOT_DIR/node_${CHANNEL}_${OS_TYPE}-${ARCH}_${FULLVERSION}.tar.gz" if [ -f "$ALGOD_INSTALL_TAR_FILE" ]; then @@ -38,14 +31,6 @@ DOCKERFILE="./docker/build/algod.Dockerfile" START_ALGOD_FILE="./docker/release/start_algod_docker.sh" ALGOD_DOCKER_INIT="./docker/release/algod_docker_init.sh" -GOLANG_VERSION=$(./scripts/get_golang_version.sh) -GOROOT=/usr/local/go -GOPATH="$HOME/go" -PATH="$GOPATH/bin:$GOROOT/bin:$PATH" - -curl "https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz" | tar -xzf - -mv go /usr/local - echo "building '$DOCKERFILE' with install file $ALGOD_INSTALL_TAR_FILE" cp "$ALGOD_INSTALL_TAR_FILE" "/tmp/$INPUT_ALGOD_TAR_FILE" cp "$ALGOD_DOCKER_INIT" /tmp diff --git a/scripts/release/mule/package/rpm/package.sh b/scripts/release/mule/package/rpm/package.sh index 3b7444b1af..4aae0bad44 100755 --- a/scripts/release/mule/package/rpm/package.sh +++ b/scripts/release/mule/package/rpm/package.sh @@ -4,28 +4,21 @@ set -ex echo "Building RPM package" -OS_TYPE="$1" -ARCH="$2" -WORKDIR="$3" - -if [ -z "$OS_TYPE" ] || [ -z "$ARCH" ] || [ -z "$WORKDIR" ]; then - echo OS, ARCH and WORKDIR variables must be defined. - exit 1 -fi - -FULLVERSION=${VERSION:-$("$WORKDIR/scripts/compute_build_number.sh" -f)} +REPO_DIR=/projects/go-algorand +ARCH=$(./scripts/archtype.sh) +OS_TYPE=$(./scripts/ostype.sh) +FULLVERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} BRANCH=${BRANCH:-$(git rev-parse --abbrev-ref HEAD)} -CHANNEL=${CHANNEL:-$("$WORKDIR/scripts/compute_branch_channel.sh" "$BRANCH")} -ALGO_BIN="$WORKDIR/tmp/node_pkgs/$OS_TYPE/$ARCH/$CHANNEL/$OS_TYPE-$ARCH/bin" +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} +ALGO_BIN="$REPO_DIR/tmp/node_pkgs/$OS_TYPE/$ARCH/$CHANNEL/${OS_TYPE}-${ARCH}/bin" # TODO: Should there be a default network? DEFAULTNETWORK=devnet -DEFAULT_RELEASE_NETWORK=$("$WORKDIR/scripts/compute_branch_release_network.sh" "${DEFAULTNETWORK}") -PKG_NAME=$("$WORKDIR/scripts/compute_package_name.sh" "${CHANNEL:-stable}") +DEFAULT_RELEASE_NETWORK=$(./scripts/compute_branch_release_network.sh "${DEFAULTNETWORK}") +PKG_NAME=$(./scripts/compute_package_name.sh "${CHANNEL:-stable}") # The following need to be exported for use in ./go-algorand/installer/rpm/algorand.spec. export DEFAULT_NETWORK export DEFAULT_RELEASE_NETWORK -REPO_DIR="$WORKDIR" export REPO_DIR export ALGO_BIN @@ -34,12 +27,12 @@ trap 'rm -rf $RPMTMP' 0 TEMPDIR=$(mktemp -d) trap 'rm -rf $TEMPDIR' 0 -< "$WORKDIR/installer/rpm/algorand.spec" \ +< "./installer/rpm/algorand.spec" \ sed -e "s,@PKG_NAME@,${PKG_NAME}," \ -e "s,@VER@,$FULLVERSION," \ > "$TEMPDIR/algorand.spec" rpmbuild --buildroot "$HOME/foo" --define "_rpmdir $RPMTMP" --define "RELEASE_GENESIS_PROCESS x$RELEASE_GENESIS_PROCESS" --define "LICENSE_FILE ./COPYING" -bb "$TEMPDIR/algorand.spec" -cp -p "$RPMTMP"/*/*.rpm "$WORKDIR/tmp/node_pkgs/$OS_TYPE/$ARCH" +cp -p "$RPMTMP"/*/*.rpm "./tmp/node_pkgs/$OS_TYPE/$ARCH" diff --git a/scripts/release/mule/sign/sign.sh b/scripts/release/mule/sign/sign.sh index ddf73836f2..272f132da1 100755 --- a/scripts/release/mule/sign/sign.sh +++ b/scripts/release/mule/sign/sign.sh @@ -7,23 +7,14 @@ echo date "+build_release begin SIGN stage %Y%m%d_%H%M%S" echo -WORKDIR="$6" - -if [ -z "$WORKDIR" ] -then - echo "WORKDIR variable must be defined." - exit 1 -fi - -OS_TYPE="$1" -ARCH_TYPE="$2" -ARCH_BIT="$3" -VERSION=${VERSION:-$4} -PKG_TYPE="$5" - +PKG_TYPE="$1" +ARCH_BIT=$(uname -m) +ARCH_TYPE=$(./scripts/archtype.sh) +OS_TYPE=$(./scripts/ostype.sh) +VERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} BRANCH=${BRANCH:-$(git rev-parse --abbrev-ref HEAD)} -CHANNEL=${CHANNEL:-$("$WORKDIR/scripts/compute_branch_channel.sh" "$BRANCH")} -PKG_DIR="$WORKDIR/tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE" +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} +PKG_DIR="./tmp/node_pkgs/$OS_TYPE/$ARCH_TYPE" SIGNING_KEY_ADDR=dev@algorand.com if ! $USE_CACHE diff --git a/scripts/release/mule/test/test.sh b/scripts/release/mule/test/test.sh index 0b3b592979..452ce6c12e 100755 --- a/scripts/release/mule/test/test.sh +++ b/scripts/release/mule/test/test.sh @@ -2,22 +2,18 @@ set -ex -export WORKDIR="$6" - -if [ -z "$WORKDIR" ] -then - echo "WORKDIR must be defined." - exit 1 -fi - -export OS_TYPE="$1" -export ARCH_TYPE="$2" -export ARCH_BIT="$3" -export VERSION=${VERSION:-$4} -export PKG_TYPE="$5" +export PKG_TYPE="$1" +ARCH_BIT=$(uname -m) +export ARCH_BIT +ARCH_TYPE=$(./scripts/archtype.sh) +export ARCH_TYPE +OS_TYPE=$(./scripts/ostype.sh) +export OS_TYPE +VERSION=${VERSION:-$(./scripts/compute_build_number.sh -f)} +export VERSION BRANCH=${BRANCH:-$(git rev-parse --abbrev-ref HEAD)} export BRANCH -CHANNEL=${CHANNEL:-$("$WORKDIR/scripts/compute_branch_channel.sh" "$BRANCH")} +CHANNEL=${CHANNEL:-$(./scripts/compute_branch_channel.sh "$BRANCH")} export CHANNEL SHA=${SHA:-$(git rev-parse HEAD)} export SHA @@ -27,5 +23,10 @@ then mule -f package-test.yaml "package-test-setup-$PKG_TYPE" fi -"$WORKDIR/scripts/release/mule/test/util/test_package.sh" +if [[ "$ARCH_TYPE" =~ "arm" ]] +then + ./scripts/release/mule/test/tests/run_tests -b "$BRANCH" -c "$CHANNEL" -h "$SHA" -r "$VERSION" +else + ./scripts/release/mule/test/util/test_package.sh +fi diff --git a/scripts/release/mule/test/tests/goal.sh b/scripts/release/mule/test/tests/goal.sh index c672d35408..b7812b4a93 100755 --- a/scripts/release/mule/test/tests/goal.sh +++ b/scripts/release/mule/test/tests/goal.sh @@ -6,7 +6,7 @@ set -ex algod -v | grep -q "${VERSION}.${CHANNEL}" mkdir -p /root/testnode -cp -p /var/lib/algorand/genesis/testnet/genesis.json /root/testnode +cp -p /var/lib/algorand/genesis.json /root/testnode goal node start -d /root/testnode goal node wait -d /root/testnode -w 120 diff --git a/scripts/release/mule/test/tests/run_tests b/scripts/release/mule/test/tests/run_tests index 3f01f7fb65..6dd3146842 100755 --- a/scripts/release/mule/test/tests/run_tests +++ b/scripts/release/mule/test/tests/run_tests @@ -53,7 +53,7 @@ fi export BRANCH export COMMIT_HASH export CHANNEL -export VERSION +export FULLVERSION for test in $(ls ./scripts/release/mule/test/tests/*.sh) do diff --git a/scripts/release/mule/test/tests/verify_pkg_string.sh b/scripts/release/mule/test/tests/verify_pkg_string.sh index 05afcd2b12..e9b602a1e9 100755 --- a/scripts/release/mule/test/tests/verify_pkg_string.sh +++ b/scripts/release/mule/test/tests/verify_pkg_string.sh @@ -9,11 +9,11 @@ SHORT_HASH=${COMMIT_HASH:0:8} # We're looking for a line that looks like the following: # -# 2.0.4.stable [rel/stable] (commit #729b125a) +# 2.0.4.stable [rel/stable] (commit #729b125a+) # # Since we're passing in the full hash, we won't using the closing paren. # Use a regex over the multi-line string. -if [[ "$STR" =~ .*"$VERSION.$CHANNEL [$BRANCH] (commit #$SHORT_HASH)".* ]] +if [[ "$STR" =~ .*"$FULLVERSION.$CHANNEL [$BRANCH] (commit #$SHORT_HASH".* ]] then echo -e "[$0] The result of \`algod -v\` is a correct match.\n$STR" exit 0 From 1424855ad2b5f6755ff3feba7e419ee06f2493da Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 9 Jul 2020 17:15:04 -0400 Subject: [PATCH 093/267] Applications (#917) Implement Algorand applications, as specified in https://github.com/algorandfoundation/specs/pull/30. --- .gitignore | 2 + cmd/goal/account.go | 35 + cmd/goal/application.go | 1028 +++++++++++++ cmd/goal/asset.go | 57 +- cmd/goal/clerk.go | 135 +- cmd/goal/commands.go | 3 + cmd/goal/common.go | 71 +- cmd/goal/interact.go | 749 ++++++++++ cmd/goal/messages.go | 10 + cmd/opdoc/opdoc.go | 19 + cmd/pingpong/runCmd.go | 22 +- cmd/tealdbg/README.md | 268 ++++ cmd/tealdbg/bundle_home_html.sh | 28 + cmd/tealdbg/cdtProto.go | 253 ++++ cmd/tealdbg/cdtSession.go | 630 ++++++++ cmd/tealdbg/cdtState.go | 786 ++++++++++ cmd/tealdbg/cdtdbg.go | 181 +++ cmd/tealdbg/debugger.go | 489 +++++++ cmd/tealdbg/debugger_test.go | 119 ++ cmd/tealdbg/dryrunRequest.go | 104 ++ cmd/tealdbg/home.html | 336 +++++ cmd/tealdbg/homepage.go | 357 +++++ cmd/tealdbg/images/cdt-controls.png | Bin 0 -> 297786 bytes cmd/tealdbg/images/cdt-screenshot.png | Bin 0 -> 211793 bytes cmd/tealdbg/images/chrome-inspect.png | Bin 0 -> 75672 bytes cmd/tealdbg/images/web-page-screenshot.png | Bin 0 -> 156619 bytes cmd/tealdbg/local.go | 487 +++++++ cmd/tealdbg/localLedger.go | 214 +++ cmd/tealdbg/local_test.go | 927 ++++++++++++ cmd/tealdbg/main.go | 265 ++++ cmd/tealdbg/remote.go | 108 ++ cmd/tealdbg/samples/balances.json | 84 ++ cmd/tealdbg/samples/branch.teal | 13 + cmd/tealdbg/samples/calls_count.teal | 22 + cmd/tealdbg/samples/calls_count_balance.json | 25 + cmd/tealdbg/samples/calls_count_txn.json | 12 + cmd/tealdbg/samples/txn.json | 17 + cmd/tealdbg/samples/txn.msgp | Bin 0 -> 296 bytes cmd/tealdbg/samples/txn_group.json | 34 + cmd/tealdbg/samples/txn_group.msgp | Bin 0 -> 592 bytes cmd/tealdbg/server.go | 130 ++ cmd/tealdbg/util.go | 128 ++ cmd/tealdbg/util_test.go | 33 + cmd/tealdbg/webdbg.go | 250 ++++ config/consensus.go | 1 - daemon/algod/api/algod.oas2.json | 654 ++++++++- daemon/algod/api/algod.oas3.yml | 787 +++++++++- daemon/algod/api/client/restClient.go | 55 +- daemon/algod/api/server/v1/handlers/errors.go | 6 + .../algod/api/server/v1/handlers/handlers.go | 184 ++- .../algod/api/server/v1/handlers/responses.go | 2 +- daemon/algod/api/server/v2/account.go | 354 +++++ daemon/algod/api/server/v2/account_test.go | 100 ++ daemon/algod/api/server/v2/dryrun.go | 509 +++++++ daemon/algod/api/server/v2/dryrun_test.go | 930 ++++++++++++ daemon/algod/api/server/v2/errors.go | 2 + .../api/server/v2/generated/private/routes.go | 223 +-- .../api/server/v2/generated/private/types.go | 222 ++- .../algod/api/server/v2/generated/routes.go | 380 +++-- daemon/algod/api/server/v2/generated/types.go | 235 ++- daemon/algod/api/server/v2/handlers.go | 224 ++- .../algod/api/server/v2/test/handlers_test.go | 158 +- daemon/algod/api/server/v2/test/helpers.go | 6 + daemon/algod/api/server/v2/utils.go | 10 + daemon/algod/api/spec/v1/model.go | 219 ++- data/basics/msgp_gen.go | 758 ++++++++-- data/basics/msgp_gen_test.go | 62 + data/basics/teal.go | 37 +- data/basics/userBalance.go | 33 +- data/basics/userBalance_test.go | 56 +- data/pools/transactionPool.go | 296 ++-- data/transactions/application.go | 137 +- data/transactions/application_test.go | 97 +- data/transactions/asset.go | 50 +- data/transactions/keyreg_test.go | 8 +- data/transactions/logic/Makefile | 2 +- data/transactions/logic/README.md | 31 +- data/transactions/logic/README_in.md | 7 +- data/transactions/logic/TEAL_opcodes.md | 35 +- data/transactions/logic/assembler.go | 55 +- data/transactions/logic/assembler_test.go | 178 ++- .../transactions/logic/backwardCompat_test.go | 27 +- data/transactions/logic/debugger.go | 12 +- data/transactions/logic/doc.go | 29 +- data/transactions/logic/doc_test.go | 2 +- data/transactions/logic/eval.go | 202 ++- data/transactions/logic/evalStateful_test.go | 208 ++- data/transactions/logic/eval_test.go | 358 +++-- data/transactions/logic/fields.go | 72 +- data/transactions/logic/fields_string.go | 35 +- data/transactions/logic/kvcow.go | 114 +- data/transactions/logic/kvcow_test.go | 192 +++ data/transactions/logic/opcodes.go | 6 + data/transactions/logic/opcodes_test.go | 2 +- data/transactions/payment_test.go | 8 +- data/transactions/transaction.go | 15 +- go.mod | 15 +- go.sum | 57 +- ledger/accountdb.go | 120 +- ledger/accountdb_test.go | 2 +- ledger/acctupdates.go | 160 +- ledger/applications.go | 238 +++ ledger/applications_test.go | 271 ++++ ledger/archival_test.go | 286 +++- ledger/catchupaccessor.go | 11 +- ledger/cow.go | 64 +- ledger/cow_test.go | 14 +- ledger/eval.go | 110 +- ledger/eval_test.go | 96 ++ ledger/ledger.go | 29 +- ledger/ledger_perf_test.go | 1297 +++++++++++++++++ libgoal/libgoal.go | 166 ++- libgoal/transactions.go | 84 ++ shared/pingpong/accounts.go | 200 ++- shared/pingpong/config.go | 2 + shared/pingpong/pingpong.go | 63 +- .../cc_agent/component/pingPongComponent.go | 5 +- .../cli/goal/expect/goalDryrunRestTest.exp | 98 ++ .../cli/goal/expect/goalExpectCommon.exp | 4 +- .../features/transactions/accountv2_test.go | 216 +++ .../features/transactions/asset_test.go | 18 +- test/scripts/e2e_subs/assets-app.sh | 282 ++++ test/scripts/e2e_subs/e2e-app-bootloader.sh | 71 + test/scripts/e2e_subs/e2e-app-closeout.sh | 67 + .../e2e_subs/e2e-app-real-assets-round.sh | 45 + test/scripts/e2e_subs/e2e-app-simple.sh | 53 + .../e2e_subs/e2e-app-stateful-global.sh | 60 + .../e2e_subs/e2e-app-stateful-local.sh | 68 + test/scripts/e2e_subs/e2e-app-x-app-reads.sh | 46 + .../e2e_subs/{e2e_teal.sh => e2e-teal.sh} | 0 test/scripts/e2e_subs/limit-swap-test.sh | 1 - test/scripts/e2e_subs/sectok-app.sh | 215 +++ test/scripts/e2e_subs/tealprogs/asa.json | 230 +++ .../e2e_subs/tealprogs/asa_approve.teal | 532 +++++++ .../scripts/e2e_subs/tealprogs/asa_clear.teal | 10 + .../e2e_subs/tealprogs/assetround.teal | 121 ++ .../e2e_subs/tealprogs/bootloader.teal.tmpl | 42 + .../scripts/e2e_subs/tealprogs/globcheck.teal | 69 + .../scripts/e2e_subs/tealprogs/globwrite.teal | 12 + test/scripts/e2e_subs/tealprogs/loccheck.teal | 79 + test/scripts/e2e_subs/tealprogs/sectok.json | 414 ++++++ .../e2e_subs/tealprogs/sectok_approve.teal | 386 +++++ .../e2e_subs/tealprogs/sectok_clear.teal | 10 + test/scripts/e2e_subs/tealprogs/upgraded.teal | 12 + .../e2e_subs/tealprogs/wrongupgrade.teal | 12 + .../scripts/e2e_subs/tealprogs/xappreads.teal | 27 + 146 files changed, 21728 insertions(+), 1578 deletions(-) create mode 100644 cmd/goal/application.go create mode 100644 cmd/goal/interact.go create mode 100644 cmd/tealdbg/README.md create mode 100755 cmd/tealdbg/bundle_home_html.sh create mode 100644 cmd/tealdbg/cdtProto.go create mode 100644 cmd/tealdbg/cdtSession.go create mode 100644 cmd/tealdbg/cdtState.go create mode 100644 cmd/tealdbg/cdtdbg.go create mode 100644 cmd/tealdbg/debugger.go create mode 100644 cmd/tealdbg/debugger_test.go create mode 100644 cmd/tealdbg/dryrunRequest.go create mode 100644 cmd/tealdbg/home.html create mode 100644 cmd/tealdbg/homepage.go create mode 100644 cmd/tealdbg/images/cdt-controls.png create mode 100644 cmd/tealdbg/images/cdt-screenshot.png create mode 100644 cmd/tealdbg/images/chrome-inspect.png create mode 100644 cmd/tealdbg/images/web-page-screenshot.png create mode 100644 cmd/tealdbg/local.go create mode 100644 cmd/tealdbg/localLedger.go create mode 100644 cmd/tealdbg/local_test.go create mode 100644 cmd/tealdbg/main.go create mode 100644 cmd/tealdbg/remote.go create mode 100644 cmd/tealdbg/samples/balances.json create mode 100644 cmd/tealdbg/samples/branch.teal create mode 100644 cmd/tealdbg/samples/calls_count.teal create mode 100644 cmd/tealdbg/samples/calls_count_balance.json create mode 100644 cmd/tealdbg/samples/calls_count_txn.json create mode 100644 cmd/tealdbg/samples/txn.json create mode 100644 cmd/tealdbg/samples/txn.msgp create mode 100644 cmd/tealdbg/samples/txn_group.json create mode 100644 cmd/tealdbg/samples/txn_group.msgp create mode 100644 cmd/tealdbg/server.go create mode 100644 cmd/tealdbg/util.go create mode 100644 cmd/tealdbg/util_test.go create mode 100644 cmd/tealdbg/webdbg.go create mode 100644 daemon/algod/api/server/v2/account.go create mode 100644 daemon/algod/api/server/v2/account_test.go create mode 100644 daemon/algod/api/server/v2/dryrun.go create mode 100644 daemon/algod/api/server/v2/dryrun_test.go create mode 100644 data/transactions/logic/kvcow_test.go create mode 100644 ledger/applications.go create mode 100644 ledger/applications_test.go create mode 100644 ledger/ledger_perf_test.go create mode 100644 test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp create mode 100644 test/e2e-go/features/transactions/accountv2_test.go create mode 100755 test/scripts/e2e_subs/assets-app.sh create mode 100755 test/scripts/e2e_subs/e2e-app-bootloader.sh create mode 100755 test/scripts/e2e_subs/e2e-app-closeout.sh create mode 100755 test/scripts/e2e_subs/e2e-app-real-assets-round.sh create mode 100755 test/scripts/e2e_subs/e2e-app-simple.sh create mode 100755 test/scripts/e2e_subs/e2e-app-stateful-global.sh create mode 100755 test/scripts/e2e_subs/e2e-app-stateful-local.sh create mode 100755 test/scripts/e2e_subs/e2e-app-x-app-reads.sh rename test/scripts/e2e_subs/{e2e_teal.sh => e2e-teal.sh} (100%) create mode 100755 test/scripts/e2e_subs/sectok-app.sh create mode 100644 test/scripts/e2e_subs/tealprogs/asa.json create mode 100644 test/scripts/e2e_subs/tealprogs/asa_approve.teal create mode 100644 test/scripts/e2e_subs/tealprogs/asa_clear.teal create mode 100644 test/scripts/e2e_subs/tealprogs/assetround.teal create mode 100644 test/scripts/e2e_subs/tealprogs/bootloader.teal.tmpl create mode 100644 test/scripts/e2e_subs/tealprogs/globcheck.teal create mode 100644 test/scripts/e2e_subs/tealprogs/globwrite.teal create mode 100644 test/scripts/e2e_subs/tealprogs/loccheck.teal create mode 100644 test/scripts/e2e_subs/tealprogs/sectok.json create mode 100644 test/scripts/e2e_subs/tealprogs/sectok_approve.teal create mode 100644 test/scripts/e2e_subs/tealprogs/sectok_clear.teal create mode 100644 test/scripts/e2e_subs/tealprogs/upgraded.teal create mode 100644 test/scripts/e2e_subs/tealprogs/wrongupgrade.teal create mode 100644 test/scripts/e2e_subs/tealprogs/xappreads.teal diff --git a/.gitignore b/.gitignore index 319b89b478..19bc82ff74 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ data/transactions/logic/*.md # Folder for collecting release assets assets +# test binaries +*.test diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 75107c9376..4429195555 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -60,6 +60,7 @@ var ( partKeyDeleteInput bool importDefault bool mnemonic string + dumpOutFile string ) func init() { @@ -88,6 +89,8 @@ func init() { accountCmd.AddCommand(partkeyInfoCmd) + accountCmd.AddCommand(dumpCmd) + // Wallet to be used for the account operation accountCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") @@ -190,6 +193,10 @@ func init() { markNonparticipatingCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") markNonparticipatingCmd.Flags().MarkDeprecated("firstRound", "use --firstvalid instead") markNonparticipatingCmd.Flags().MarkDeprecated("validRounds", "use --validrounds instead") + + dumpCmd.Flags().StringVarP(&dumpOutFile, "outfile", "o", "", "Save balance record to specified output file") + dumpCmd.Flags().StringVarP(&accountAddress, "address", "a", "", "Account address to retrieve balance (required)") + balanceCmd.MarkFlagRequired("address") } func scLeaseBytes(cmd *cobra.Command) (leaseBytes [32]byte) { @@ -530,6 +537,34 @@ var balanceCmd = &cobra.Command{ }, } +var dumpCmd = &cobra.Command{ + Use: "dump", + Short: "Dump the balance record for the specified account", + Long: `Dump the balance record for the specified account to terminal as JSON or to a file as MessagePack.`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + dataDir := ensureSingleDataDir() + client := ensureAlgodClient(dataDir) + rawAddress, err := basics.UnmarshalChecksumAddress(accountAddress) + if err != nil { + reportErrorf(errorParseAddr, err) + } + accountData, err := client.AccountData(accountAddress) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + br := basics.BalanceRecord{Addr: rawAddress, AccountData: accountData} + if len(dumpOutFile) > 0 { + data := protocol.Encode(&br) + writeFile(dumpOutFile, data, 0644) + } else { + data := protocol.EncodeJSON(&br) + fmt.Println(string(data)) + } + }, +} + var rewardsCmd = &cobra.Command{ Use: "rewards", Short: "Retrieve the rewards for the specified account", diff --git a/cmd/goal/application.go b/cmd/goal/application.go new file mode 100644 index 0000000000..692954612e --- /dev/null +++ b/cmd/goal/application.go @@ -0,0 +1,1028 @@ +// 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 . + +package main + +import ( + "encoding/base32" + "encoding/base64" + "encoding/binary" + "fmt" + "os" + "strconv" + "strings" + "unicode" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/crypto" + "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/libgoal" + "github.com/algorand/go-algorand/protocol" +) + +var ( + appIdx uint64 + appCreator string + + approvalProgFile string + clearProgFile string + + approvalProgRawFile string + clearProgRawFile string + + createOnCompletion string + + localSchemaUints uint64 + localSchemaByteSlices uint64 + + globalSchemaUints uint64 + globalSchemaByteSlices uint64 + + // Cobra only has a slice helper for uint, not uint64, so we'll parse + // uint64s from strings for now. 4bn transactions and using a 32-bit + // platform seems not so far-fetched? + foreignApps []string + appStrAccounts []string + + appArgs []string + appInputFilename string + + fetchLocal bool + fetchGlobal bool + guessFormat bool +) + +func init() { + appCmd.AddCommand(createAppCmd) + appCmd.AddCommand(deleteAppCmd) + appCmd.AddCommand(updateAppCmd) + appCmd.AddCommand(callAppCmd) + appCmd.AddCommand(optInAppCmd) + appCmd.AddCommand(closeOutAppCmd) + appCmd.AddCommand(clearAppCmd) + appCmd.AddCommand(readStateAppCmd) + + appCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") + appCmd.PersistentFlags().StringSliceVar(&appArgs, "app-arg", nil, "Args to encode for application transactions (all will be encoded to a byte slice). For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.") + appCmd.PersistentFlags().StringSliceVar(&foreignApps, "foreign-app", nil, "Indexes of other apps whose global state is read in this transaction") + appCmd.PersistentFlags().StringSliceVar(&appStrAccounts, "app-account", nil, "Accounts that may be accessed from application logic") + appCmd.PersistentFlags().StringVarP(&appInputFilename, "app-input", "i", "", "JSON file containing encoded arguments and inputs (mutually exclusive with app-arg-b64 and app-account)") + + appCmd.PersistentFlags().StringVar(&approvalProgFile, "approval-prog", "", "(Uncompiled) TEAL assembly program filename for approving/rejecting transactions") + appCmd.PersistentFlags().StringVar(&clearProgFile, "clear-prog", "", "(Uncompiled) TEAL assembly program filename for updating application state when a user clears their local state") + + appCmd.PersistentFlags().StringVar(&approvalProgRawFile, "approval-prog-raw", "", "Compiled TEAL program filename for approving/rejecting transactions") + appCmd.PersistentFlags().StringVar(&clearProgRawFile, "clear-prog-raw", "", "Compiled TEAL program filename for updating application state when a user clears their local state") + + createAppCmd.Flags().Uint64Var(&globalSchemaUints, "global-ints", 0, "Maximum number of integer values that may be stored in the global key/value store. Immutable.") + createAppCmd.Flags().Uint64Var(&globalSchemaByteSlices, "global-byteslices", 0, "Maximum number of byte slices that may be stored in the global key/value store. Immutable.") + createAppCmd.Flags().Uint64Var(&localSchemaUints, "local-ints", 0, "Maximum number of integer values that may be stored in local (per-account) key/value stores for this app. Immutable.") + createAppCmd.Flags().Uint64Var(&localSchemaByteSlices, "local-byteslices", 0, "Maximum number of byte slices that may be stored in local (per-account) key/value stores for this app. Immutable.") + createAppCmd.Flags().StringVar(&appCreator, "creator", "", "Account to create the application") + createAppCmd.Flags().StringVar(&createOnCompletion, "on-completion", "NoOp", "OnCompletion action for application transaction") + + callAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to call app from") + optInAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to opt in") + closeOutAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to opt out") + clearAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to clear app state for") + deleteAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send delete transaction from") + readStateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to fetch state from") + updateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send update transaction from") + + // Can't use PersistentFlags on the root because for some reason marking + // a root command as required with MarkPersistentFlagRequired isn't + // working + callAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + optInAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + closeOutAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + clearAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + deleteAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + readStateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + updateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + + // Add common transaction flags to all txn-generating app commands + addTxnFlags(createAppCmd) + addTxnFlags(deleteAppCmd) + addTxnFlags(updateAppCmd) + addTxnFlags(callAppCmd) + addTxnFlags(optInAppCmd) + addTxnFlags(closeOutAppCmd) + addTxnFlags(clearAppCmd) + + readStateAppCmd.Flags().BoolVar(&fetchLocal, "local", false, "Fetch account-specific state for this application. `--from` address is required when using this flag") + readStateAppCmd.Flags().BoolVar(&fetchGlobal, "global", false, "Fetch global state for this application.") + readStateAppCmd.Flags().BoolVar(&guessFormat, "guess-format", false, "Format application state using heuristics to guess data encoding.") + + createAppCmd.MarkFlagRequired("creator") + createAppCmd.MarkFlagRequired("global-ints") + createAppCmd.MarkFlagRequired("global-byteslices") + createAppCmd.MarkFlagRequired("local-ints") + createAppCmd.MarkFlagRequired("local-byteslices") + + optInAppCmd.MarkFlagRequired("app-id") + optInAppCmd.MarkFlagRequired("from") + + callAppCmd.MarkFlagRequired("app-id") + callAppCmd.MarkFlagRequired("from") + + closeOutAppCmd.MarkFlagRequired("app-id") + closeOutAppCmd.MarkFlagRequired("from") + + clearAppCmd.MarkFlagRequired("app-id") + clearAppCmd.MarkFlagRequired("from") + + deleteAppCmd.MarkFlagRequired("app-id") + deleteAppCmd.MarkFlagRequired("from") + + updateAppCmd.MarkFlagRequired("app-id") + updateAppCmd.MarkFlagRequired("from") + + readStateAppCmd.MarkFlagRequired("app-id") +} + +type appCallArg struct { + Encoding string `codec:"encoding"` + Value string `codec:"value"` +} + +type appCallInputs struct { + Accounts []string `codec:"accounts"` + ForeignApps []uint64 `codec:"foreignapps"` + Args []appCallArg `codec:"args"` +} + +func getForeignApps() []uint64 { + out := make([]uint64, len(foreignApps)) + for i, app := range foreignApps { + parsed, err := strconv.ParseUint(app, 10, 64) + if err != nil { + reportErrorf("Could not parse foreign app id: %v", err) + } + out[i] = parsed + } + return out +} + +func parseAppArg(arg appCallArg) (rawValue []byte, parseErr error) { + switch arg.Encoding { + case "str", "string": + rawValue = []byte(arg.Value) + case "int", "integer": + num, err := strconv.ParseUint(arg.Value, 10, 64) + if err != nil { + parseErr = fmt.Errorf("Could not parse uint64 from string (%s): %v", arg.Value, err) + return + } + ibytes := make([]byte, 8) + binary.BigEndian.PutUint64(ibytes, num) + rawValue = ibytes + case "addr", "address": + addr, err := basics.UnmarshalChecksumAddress(arg.Value) + if err != nil { + parseErr = fmt.Errorf("Could not unmarshal checksummed address from string (%s): %v", arg.Value, err) + return + } + rawValue = addr[:] + case "b32", "base32", "byte base32": + data, err := base32.StdEncoding.DecodeString(arg.Value) + if err != nil { + parseErr = fmt.Errorf("Could not decode base32-encoded string (%s): %v", arg.Value, err) + return + } + rawValue = data + case "b64", "base64", "byte base64": + data, err := base64.StdEncoding.DecodeString(arg.Value) + if err != nil { + parseErr = fmt.Errorf("Could not decode base64-encoded string (%s): %v", arg.Value, err) + return + } + rawValue = data + default: + parseErr = fmt.Errorf("Unknown encoding: %s", arg.Encoding) + } + return +} + +func parseAppInputs(inputs appCallInputs) (args [][]byte, accounts []string, foreignApps []uint64) { + accounts = inputs.Accounts + foreignApps = inputs.ForeignApps + args = make([][]byte, len(inputs.Args)) + for i, arg := range inputs.Args { + rawValue, err := parseAppArg(arg) + if err != nil { + reportErrorf("Could not decode input at index %d: %v", i, err) + } + args[i] = rawValue + } + return +} + +func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint64) { + var inputs appCallInputs + f, err := os.Open(appInputFilename) + if err != nil { + reportErrorf("Could not open app input JSON file: %v", err) + } + + dec := protocol.NewJSONDecoder(f) + err = dec.Decode(&inputs) + if err != nil { + reportErrorf("Could not decode app input JSON file: %v", err) + } + + return parseAppInputs(inputs) +} + +func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64) { + if (appArgs != nil || appStrAccounts != nil || foreignApps != nil) && appInputFilename != "" { + reportErrorf("Cannot specify both command-line arguments/accounts and JSON input filename") + } + if appInputFilename != "" { + return processAppInputFile() + } + + var encodedArgs []appCallArg + for _, arg := range appArgs { + encodingValue := strings.SplitN(arg, ":", 2) + if len(encodingValue) != 2 { + reportErrorf("all arguments should be of the form 'encoding:value'") + } + encodedArg := appCallArg{ + Encoding: encodingValue[0], + Value: encodingValue[1], + } + encodedArgs = append(encodedArgs, encodedArg) + } + + inputs := appCallInputs{ + Accounts: appStrAccounts, + ForeignApps: getForeignApps(), + Args: encodedArgs, + } + + return parseAppInputs(inputs) +} + +var appCmd = &cobra.Command{ + Use: "app", + Short: "Manage applications", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + // If no arguments passed, we should fallback to help + cmd.HelpFunc()(cmd, args) + }, +} + +func mustParseOnCompletion(ocString string) (oc transactions.OnCompletion) { + switch strings.ToLower(ocString) { + case "noop": + return transactions.NoOpOC + case "optin": + return transactions.OptInOC + case "closeout": + return transactions.CloseOutOC + case "clearstate": + return transactions.ClearStateOC + case "updateapplication": + return transactions.UpdateApplicationOC + case "deleteapplication": + return transactions.DeleteApplicationOC + default: + reportErrorf("unknown value for --on-completion: %s (possible values: {NoOp, OptIn, CloseOut, ClearState, UpdateApplication, DeleteApplication})", ocString) + return + } +} + +func mustParseProgArgs() (approval []byte, clear []byte) { + // Ensure we don't have ambiguous or all empty args + if (approvalProgFile == "") == (approvalProgRawFile == "") { + reportErrorf(errorApprovProgArgsRequired) + } + if (clearProgFile == "") == (clearProgRawFile == "") { + reportErrorf(errorClearProgArgsRequired) + } + + if approvalProgFile != "" { + approval = assembleFile(approvalProgFile) + } else { + approval = mustReadFile(approvalProgRawFile) + } + + if clearProgFile != "" { + clear = assembleFile(clearProgFile) + } else { + clear = mustReadFile(clearProgRawFile) + } + + return +} + +var createAppCmd = &cobra.Command{ + Use: "create", + Short: "Create an application", + Long: `Issue a transaction that creates an application`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Construct schemas from args + localSchema := basics.StateSchema{ + NumUint: localSchemaUints, + NumByteSlice: localSchemaByteSlices, + } + + globalSchema := basics.StateSchema{ + NumUint: globalSchemaUints, + NumByteSlice: globalSchemaByteSlices, + } + + // Parse transaction parameters + approvalProg, clearProg := mustParseProgArgs() + onCompletion := mustParseOnCompletion(createOnCompletion) + appArgs, appAccounts, foreignApps := getAppInputs() + + switch onCompletion { + case transactions.CloseOutOC, transactions.ClearStateOC: + reportWarnf("'--on-completion %s' may be ill-formed for 'goal app create'", createOnCompletion) + } + + tx, err := client.MakeUnsignedAppCreateTx(onCompletion, approvalProg, clearProg, globalSchema, localSchema, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(appCreator, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + if outFilename == "" { + // Broadcast + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + reportInfof("Attempting to create app (approval size %d, hash %v; clear size %d, hash %v)", len(approvalProg), crypto.HashObj(logic.Program(approvalProg)), len(clearProg), crypto.HashObj(logic.Program(clearProg))) + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + txn, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + if txn.TransactionResults != nil && txn.TransactionResults.CreatedAppIndex != 0 { + reportInfof("Created app with app index %d", txn.TransactionResults.CreatedAppIndex) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + // Write transaction to file + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var updateAppCmd = &cobra.Command{ + Use: "update", + Short: "Update an application's programs", + Long: `Issue a transaction that updates an application's ApprovalProgram and ClearStateProgram`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + approvalProg, clearProg := mustParseProgArgs() + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppUpdateTx(appIdx, appArgs, appAccounts, foreignApps, approvalProg, clearProg) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + reportInfof("Attempting to update app (approval size %d, hash %v; clear size %d, hash %v)", len(approvalProg), crypto.HashObj(logic.Program(approvalProg)), len(clearProg), crypto.HashObj(logic.Program(clearProg))) + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var optInAppCmd = &cobra.Command{ + Use: "optin", + Short: "Opt in to an application", + Long: `Opt an account in to an application, allocating local state in your account`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppOptInTx(appIdx, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + // Report tx details to user + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var closeOutAppCmd = &cobra.Command{ + Use: "closeout", + Short: "Close out of an application", + Long: `Close an account out of an application, removing local state from your account. The application must still exist. If it doesn't, use 'goal app clear'.`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppCloseOutTx(appIdx, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + // Report tx details to user + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var clearAppCmd = &cobra.Command{ + Use: "clear", + Short: "Clear out an application's state in your account", + Long: `Remove any local state from your account associated with an application. The application does not need to exist anymore.`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppClearStateTx(appIdx, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + // Report tx details to user + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var callAppCmd = &cobra.Command{ + Use: "call", + Short: "Call an application", + Long: `Call an application, invoking application-specific functionality`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppNoOpTx(appIdx, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + // Report tx details to user + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +var deleteAppCmd = &cobra.Command{ + Use: "delete", + Short: "Delete an application", + Long: `Delete an application, removing the global state and other application parameters from the creator's account`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Parse transaction parameters + appArgs, appAccounts, foreignApps := getAppInputs() + + tx, err := client.MakeUnsignedAppDeleteTx(appIdx, appArgs, appAccounts, foreignApps) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + // Broadcast or write transaction to file + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + // Report tx details to user + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + _, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + } + } else { + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, tx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + } + }, +} + +func printable(str string) bool { + for _, r := range str { + if !unicode.IsPrint(r) { + return false + } + } + return true +} + +func heuristicFormatStr(str string) string { + if printable(str) { + return str + } + + if len(str) == 32 { + var addr basics.Address + copy(addr[:], []byte(str)) + return addr.String() + } + + return str +} + +func heuristicFormatKey(key string) string { + return heuristicFormatStr(key) +} + +func heuristicFormatVal(val basics.TealValue) basics.TealValue { + if val.Type == basics.TealUintType { + return val + } + val.Bytes = heuristicFormatStr(val.Bytes) + return val +} + +func heuristicFormat(state map[string]basics.TealValue) map[string]basics.TealValue { + result := make(map[string]basics.TealValue) + for k, v := range state { + result[heuristicFormatKey(k)] = heuristicFormatVal(v) + } + return result +} + +var readStateAppCmd = &cobra.Command{ + Use: "read", + Short: "Read local or global state for an application", + Long: `Read global or local (account-specific) state for an application`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + // Ensure exactly one of --local or --global is specified + if fetchLocal == fetchGlobal { + reportErrorf(errorLocalGlobal) + } + + // If fetching local state, ensure account is specified + if fetchLocal && account == "" { + reportErrorf(errorLocalStateRequiresAccount) + } + + if fetchLocal { + // Fetching local state. Get account information + ad, err := client.AccountData(account) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get application local state + local, ok := ad.AppLocalStates[basics.AppIndex(appIdx)] + if !ok { + reportErrorf(errorAccountNotOptedInToApp, account, appIdx) + } + + kv := local.KeyValue + if guessFormat { + kv = heuristicFormat(kv) + } + + // Encode local state to json, print, and exit + enc := protocol.EncodeJSON(kv) + + // Print to stdout + os.Stdout.Write(enc) + return + } + + if fetchGlobal { + // Fetching global state. Get application creator + app, err := client.ApplicationInformation(appIdx) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get creator information + ad, err := client.AccountData(app.Params.Creator) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get app params + params, ok := ad.AppParams[basics.AppIndex(appIdx)] + if !ok { + reportErrorf(errorNoSuchApplication, appIdx) + } + + kv := params.GlobalState + if guessFormat { + kv = heuristicFormat(kv) + } + + // Encode global state to json, print, and exit + enc := protocol.EncodeJSON(kv) + + // Print to stdout + os.Stdout.Write(enc) + return + } + + // Should be unreachable + return + }, +} diff --git a/cmd/goal/asset.go b/cmd/goal/asset.go index 59b1ee8f81..66e88f06ac 100644 --- a/cmd/goal/asset.go +++ b/cmd/goal/asset.go @@ -61,18 +61,8 @@ func init() { createAssetCmd.Flags().BoolVar(&assetFrozen, "defaultfrozen", false, "Freeze or not freeze holdings by default") createAssetCmd.Flags().StringVar(&assetUnitName, "unitname", "", "Name for the unit of asset") createAssetCmd.Flags().StringVar(&assetName, "name", "", "Name for the entire asset") - createAssetCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - createAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - createAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") - createAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") createAssetCmd.Flags().StringVar(&assetURL, "asseturl", "", "URL where user can access more information about the asset (max 32 bytes)") createAssetCmd.Flags().StringVar(&assetMetadataHashBase64, "assetmetadatab64", "", "base-64 encoded 32-byte commitment to asset metadata") - createAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") - createAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") - createAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - createAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - createAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - createAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") createAssetCmd.MarkFlagRequired("total") createAssetCmd.MarkFlagRequired("creator") @@ -80,16 +70,6 @@ func init() { destroyAssetCmd.Flags().StringVar(&assetCreator, "creator", "", "Account address for asset to destroy") destroyAssetCmd.Flags().Uint64Var(&assetID, "assetid", 0, "Asset ID to destroy") destroyAssetCmd.Flags().StringVar(&assetUnitName, "asset", "", "Unit name of asset to destroy") - destroyAssetCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - destroyAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - destroyAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") - destroyAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - destroyAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") - destroyAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") - destroyAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - destroyAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - destroyAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - destroyAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") configAssetCmd.Flags().StringVar(&assetManager, "manager", "", "Manager account to issue the config transaction (defaults to creator)") configAssetCmd.Flags().StringVar(&assetCreator, "creator", "", "Account address for asset to configure") @@ -99,16 +79,6 @@ func init() { configAssetCmd.Flags().StringVar(&assetNewReserve, "new-reserve", "", "New reserve address") configAssetCmd.Flags().StringVar(&assetNewFreezer, "new-freezer", "", "New freeze address") configAssetCmd.Flags().StringVar(&assetNewClawback, "new-clawback", "", "New clawback address") - configAssetCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - configAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - configAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") - configAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - configAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") - configAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") - configAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - configAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - configAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - configAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") configAssetCmd.MarkFlagRequired("manager") sendAssetCmd.Flags().StringVar(&assetClawback, "clawback", "", "Address to issue a clawback transaction from (defaults to no clawback)") @@ -119,16 +89,6 @@ func init() { sendAssetCmd.Flags().StringVarP(&toAddress, "to", "t", "", "Address to send to money to (required)") sendAssetCmd.Flags().Uint64VarP(&amount, "amount", "a", 0, "The amount to be transferred (required), in base units of the asset.") sendAssetCmd.Flags().StringVarP(&closeToAddress, "close-to", "c", "", "Close asset account and send remainder to this address") - sendAssetCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - sendAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - sendAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") - sendAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - sendAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") - sendAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") - sendAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - sendAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - sendAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - sendAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") sendAssetCmd.MarkFlagRequired("to") sendAssetCmd.MarkFlagRequired("amount") @@ -138,20 +98,17 @@ func init() { freezeAssetCmd.Flags().StringVar(&assetUnitName, "asset", "", "Unit name of the asset being frozen") freezeAssetCmd.Flags().StringVar(&account, "account", "", "Account address to freeze/unfreeze") freezeAssetCmd.Flags().BoolVar(&assetFrozen, "freeze", false, "Freeze or unfreeze") - freezeAssetCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - freezeAssetCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - freezeAssetCmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") - freezeAssetCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - freezeAssetCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") - freezeAssetCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") - freezeAssetCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - freezeAssetCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - freezeAssetCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - freezeAssetCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") freezeAssetCmd.MarkFlagRequired("freezer") freezeAssetCmd.MarkFlagRequired("account") freezeAssetCmd.MarkFlagRequired("freeze") + // Add common transaction flags to all txn-generating asset commands + addTxnFlags(createAssetCmd) + addTxnFlags(destroyAssetCmd) + addTxnFlags(configAssetCmd) + addTxnFlags(sendAssetCmd) + addTxnFlags(freezeAssetCmd) + infoAssetCmd.Flags().Uint64Var(&assetID, "assetid", 0, "ID of the asset to look up") infoAssetCmd.Flags().StringVar(&assetUnitName, "asset", "", "Unit name of the asset to look up") infoAssetCmd.Flags().StringVar(&assetCreator, "creator", "", "Account address of the asset creator") diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index a8aea32754..f7714f1027 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" + generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" @@ -42,27 +43,22 @@ var ( toAddress string account string amount uint64 - fee uint64 txFilename string - outFilename string rejectsFilename string - noteBase64 string - noteText string - lease string - sign bool closeToAddress string - noWaitAfterSend bool noProgramOutput bool signProgram bool programSource string argB64Strings []string disassemble bool + verbose bool progByteFile string logicSigFile string timeStamp int64 protoVersion string rekeyToAddress string signerAddress string + rawOutput bool ) func init() { @@ -74,6 +70,7 @@ func init() { clerkCmd.AddCommand(splitCmd) clerkCmd.AddCommand(compileCmd) clerkCmd.AddCommand(dryrunCmd) + clerkCmd.AddCommand(dryrunRemoteCmd) // Wallet to be used for the clerk operation clerkCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") @@ -82,26 +79,18 @@ func init() { sendCmd.Flags().StringVarP(&account, "from", "f", "", "Account address to send the money from (If not specified, uses default account)") sendCmd.Flags().StringVarP(&toAddress, "to", "t", "", "Address to send to money to (required)") sendCmd.Flags().Uint64VarP(&amount, "amount", "a", 0, "The amount to be transferred (required), in microAlgos") - sendCmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") - sendCmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") - sendCmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") - sendCmd.Flags().Uint64VarP(&numValidRounds, "validrounds", "v", 0, "The validity period for the transaction, used to calculate lastvalid") - sendCmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") - sendCmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") - sendCmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") - sendCmd.Flags().StringVarP(&outFilename, "out", "o", "", "Dump an unsigned tx to the given file. In order to dump a signed transaction, pass -s") - sendCmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") sendCmd.Flags().StringVarP(&closeToAddress, "close-to", "c", "", "Close account and send remainder to this address") - sendCmd.Flags().StringVar(&rekeyToAddress, "rekey-to", "", "Rekey account to the given authorization address. (Future transactions from this account will need to be signed with the new key.)") - sendCmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") + sendCmd.Flags().StringVar(&rekeyToAddress, "rekey-to", "", "Rekey account to the given spending key/address. (Future transactions from this account will need to be signed with the new key.)") sendCmd.Flags().StringVarP(&programSource, "from-program", "F", "", "Program source to use as account logic") sendCmd.Flags().StringVarP(&progByteFile, "from-program-bytes", "P", "", "Program binary to use as account logic") sendCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "base64 encoded args to pass to transaction logic") sendCmd.Flags().StringVarP(&logicSigFile, "logic-sig", "L", "", "LogicSig to apply to transaction") - sendCmd.MarkFlagRequired("to") sendCmd.MarkFlagRequired("amount") + // Add common transaction flags + addTxnFlags(sendCmd) + // rawsend flags rawsendCmd.Flags().StringVarP(&txFilename, "filename", "f", "", "Filename of file containing raw transactions") rawsendCmd.Flags().StringVarP(&rejectsFilename, "rejects", "r", "", "Filename for writing rejects to (default is txFilename.rej)") @@ -137,6 +126,12 @@ func init() { dryrunCmd.Flags().StringVarP(&txFilename, "txfile", "t", "", "transaction or transaction-group to test") dryrunCmd.Flags().StringVarP(&protoVersion, "proto", "P", "", "consensus protocol version id string") dryrunCmd.MarkFlagRequired("txfile") + + dryrunRemoteCmd.Flags().StringVarP(&txFilename, "dryrun-state", "D", "", "dryrun request object to run") + dryrunRemoteCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "print more info") + dryrunRemoteCmd.Flags().BoolVarP(&rawOutput, "raw", "r", false, "output raw response from algod") + dryrunRemoteCmd.MarkFlagRequired("dryrun-state") + } var clerkCmd = &cobra.Command{ @@ -213,12 +208,12 @@ func writeTxnToFile(client libgoal.Client, signTx bool, dataDir string, walletNa return writeFile(filename, protocol.Encode(&stxn), 0600) } -func getProgramArgs() [][]byte { - if len(argB64Strings) == 0 { +func getB64Args(args []string) [][]byte { + if len(args) == 0 { return nil } - programArgs := make([][]byte, len(argB64Strings)) - for i, argstr := range argB64Strings { + programArgs := make([][]byte, len(args)) + for i, argstr := range args { if argstr == "" { programArgs[i] = []byte{} continue @@ -230,6 +225,11 @@ func getProgramArgs() [][]byte { } } return programArgs + +} + +func getProgramArgs() [][]byte { + return getB64Args(argB64Strings) } func parseNoteField(cmd *cobra.Command) []byte { @@ -410,9 +410,19 @@ var sendCmd = &cobra.Command{ } } } else { - err = writeFile(outFilename, protocol.Encode(&stx), 0600) - if err != nil { - reportErrorf(err.Error()) + if dumpForDryrun { + // Write dryrun data to file + proto, _ := getProto(protoVersion) + data, err := libgoal.MakeDryrunStateBytes(client, stx, []transactions.SignedTxn{}, string(proto), dumpForDryrunFormat.String()) + if err != nil { + reportErrorf(err.Error()) + } + writeFile(outFilename, data, 0600) + } else { + err = writeFile(outFilename, protocol.Encode(&stx), 0600) + if err != nil { + reportErrorf(err.Error()) + } } } }, @@ -783,6 +793,14 @@ var splitCmd = &cobra.Command{ }, } +func mustReadFile(fname string) []byte { + contents, err := readFile(fname) + if err != nil { + reportErrorf("%s: %s\n", fname, err) + } + return contents +} + func assembleFile(fname string) (program []byte) { text, err := readFile(fname) if err != nil { @@ -931,10 +949,10 @@ var dryrunCmd = &cobra.Command{ sb := strings.Builder{} ep = logic.EvalParams{ Txn: &txn, + GroupIndex: i, Proto: ¶ms, Trace: &sb, TxnGroup: txgroup, - GroupIndex: i, } pass, err := logic.Eval(txn.Lsig.Logic, ep) // TODO: optionally include `inspect` output here? @@ -951,3 +969,66 @@ var dryrunCmd = &cobra.Command{ }, } + +var dryrunRemoteCmd = &cobra.Command{ + Use: "dryrun-remote", + Short: "test a program with algod's dryrun REST endpoint", + Long: "Test a TEAL program with algod's dryrun REST endpoint under various conditions and verbosity.", + Run: func(cmd *cobra.Command, args []string) { + data, err := readFile(txFilename) + if err != nil { + reportErrorf(fileReadError, txFilename, err) + } + + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + resp, err := client.Dryrun(data) + if err != nil { + reportErrorf("dryrun-remote: %s\n", err.Error()) + } + if rawOutput { + fmt.Fprintf(os.Stdout, string(protocol.EncodeJSON(&resp))) + return + } + + stackToString := func(stack []generatedV2.TealValue) string { + result := make([]string, len(stack)) + for i, sv := range stack { + if sv.Type == uint64(basics.TealBytesType) { + result[i] = heuristicFormatStr(sv.Bytes) + } else { + result[i] = fmt.Sprintf("%d", sv.Uint) + } + } + return strings.Join(result, " ") + } + if len(resp.Txns) > 0 { + for i, txnResult := range resp.Txns { + var msgs []string + var trace []generatedV2.DryrunState + if txnResult.AppCallMessages != nil && len(*txnResult.AppCallMessages) > 0 { + msgs = *txnResult.AppCallMessages + if txnResult.AppCallTrace != nil { + trace = *txnResult.AppCallTrace + } + } else if txnResult.LogicSigMessages != nil && len(*txnResult.LogicSigMessages) > 0 { + msgs = *txnResult.LogicSigMessages + if txnResult.LogicSigTrace != nil { + trace = *txnResult.LogicSigTrace + } + } + fmt.Fprintf(os.Stdout, "tx[%d] messages:\n", i) + for _, msg := range msgs { + fmt.Fprintf(os.Stdout, "%s\n", msg) + } + if verbose && len(trace) > 0 { + fmt.Fprintf(os.Stdout, "tx[%d] trace:\n", i) + for _, item := range trace { + fmt.Fprintf(os.Stdout, "%4d (%04x): %s [%s]\n", + item.Line, item.Pc, txnResult.Disassembly[item.Line-1], stackToString(item.Stack)) + } + } + } + } + }, +} diff --git a/cmd/goal/commands.go b/cmd/goal/commands.go index 33b8b490ae..b572eece0d 100644 --- a/cmd/goal/commands.go +++ b/cmd/goal/commands.go @@ -92,6 +92,9 @@ func init() { // completion.go rootCmd.AddCommand(completionCmd) + // application.go + rootCmd.AddCommand(appCmd) + // Config defaultDataDirValue := []string{""} rootCmd.PersistentFlags().StringArrayVarP(&dataDirs, "datadir", "d", defaultDataDirValue, "Data directory for the node") diff --git a/cmd/goal/common.go b/cmd/goal/common.go index bd54d9d077..30c793825e 100644 --- a/cmd/goal/common.go +++ b/cmd/goal/common.go @@ -16,7 +16,12 @@ package main -import "github.com/spf13/cobra" +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" +) const ( stdoutFilenameValue = "-" @@ -34,3 +39,67 @@ var lastValid uint64 // numValidRounds specifies validity period for a transaction and used to calculate last valid round var numValidRounds uint64 // also used in account and asset + +var ( + fee uint64 + outFilename string + sign bool + noteBase64 string + noteText string + lease string + noWaitAfterSend bool + dumpForDryrun bool +) + +var dumpForDryrunFormat cobraStringValue = *makeCobraStringValue("json", []string{"msgp"}) + +func addTxnFlags(cmd *cobra.Command) { + cmd.Flags().Uint64Var(&fee, "fee", 0, "The transaction fee (automatically determined by default), in microAlgos") + cmd.Flags().Uint64Var(&firstValid, "firstvalid", 0, "The first round where the transaction may be committed to the ledger") + cmd.Flags().Uint64Var(&numValidRounds, "validrounds", 0, "The number of rounds for which the transaction will be valid") + cmd.Flags().Uint64Var(&lastValid, "lastvalid", 0, "The last round where the transaction may be committed to the ledger") + cmd.Flags().StringVarP(&outFilename, "out", "o", "", "Write transaction to this file") + cmd.Flags().BoolVarP(&sign, "sign", "s", false, "Use with -o to indicate that the dumped transaction should be signed") + cmd.Flags().StringVar(¬eBase64, "noteb64", "", "Note (URL-base64 encoded)") + cmd.Flags().StringVarP(¬eText, "note", "n", "", "Note text (ignored if --noteb64 used also)") + cmd.Flags().StringVarP(&lease, "lease", "x", "", "Lease value (base64, optional): no transaction may also acquire this lease until lastvalid") + cmd.Flags().BoolVarP(&noWaitAfterSend, "no-wait", "N", false, "Don't wait for transaction to commit") + cmd.Flags().BoolVar(&dumpForDryrun, "dryrun-dump", false, "Dump in dryrun format acceptable by dryrun REST api") + cmd.Flags().Var(&dumpForDryrunFormat, "dryrun-dump-format", "Dryrun dump format: "+dumpForDryrunFormat.AllowedString()) +} + +type cobraStringValue struct { + value string + allowed []string + isSet bool +} + +func makeCobraStringValue(value string, others []string) *cobraStringValue { + c := new(cobraStringValue) + c.value = value + c.allowed = make([]string, 0, len(others)+1) + c.allowed = append(c.allowed, value) + for _, s := range others { + c.allowed = append(c.allowed, s) + } + return c +} + +func (c *cobraStringValue) String() string { return c.value } +func (c *cobraStringValue) Type() string { return "string" } +func (c *cobraStringValue) IsSet() bool { return c.isSet } + +func (c *cobraStringValue) Set(other string) error { + for _, s := range c.allowed { + if other == s { + c.value = other + c.isSet = true + return nil + } + } + return fmt.Errorf("value %s not allowed", other) +} + +func (c *cobraStringValue) AllowedString() string { + return strings.Join(c.allowed, ", ") +} diff --git a/cmd/goal/interact.go b/cmd/goal/interact.go new file mode 100644 index 0000000000..6b4785b338 --- /dev/null +++ b/cmd/goal/interact.go @@ -0,0 +1,749 @@ +// 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 . + +package main + +import ( + "encoding/base32" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" +) + +var ( + appHdr string +) + +func init() { + appCmd.AddCommand(appInteractCmd) + + appInteractCmd.AddCommand(appExecuteCmd) + appInteractCmd.AddCommand(appQueryCmd) + appInteractCmd.PersistentFlags().StringVarP(&appHdr, "header", "", "", "Application header") + + appQueryCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + appQueryCmd.Flags().StringVarP(&account, "from", "f", "", "Account to query state for (if omitted, query from global state)") + appQueryCmd.Flags().SetInterspersed(false) + appQueryCmd.MarkFlagRequired("app-id") + + appExecuteCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID (if omitted, zero, which creates an application)") + appExecuteCmd.Flags().StringVarP(&account, "from", "f", "", "Account to execute interaction from") + appExecuteCmd.Flags().SetInterspersed(false) + appExecuteCmd.MarkFlagRequired("from") +} + +var appInteractCmd = &cobra.Command{ + Use: "interact", + Short: "Interact with an application", + Args: cobra.ArbitraryArgs, + Run: func(cmd *cobra.Command, args []string) { + // If no arguments passed, we should fallback to help + cmd.HelpFunc()(cmd, args) + }, +} + +type appInteractDatum interface { + kind() string + help() string + pseudo() bool +} + +func helpList(help map[string]appInteractDatum) string { + var names []string + largestName := 0 + largestKind := 0 + for k, v := range help { + if v.pseudo() { + continue + } + if len(k) > largestName { + largestName = len(k) + } + if len(v.kind()) > largestKind { + largestKind = len(v.kind()) + } + names = append(names, k) + } + + namesize := "%-" + fmt.Sprintf("%d", largestName+3) + "s" + kindsize := "%-" + fmt.Sprintf("%d", largestKind+3) + "s" + fmtstr := " " + namesize + " " + kindsize + " " + "%s" + + var entries []string + for k, v := range help { + if v.pseudo() { + continue + } + entries = append(entries, fmt.Sprintf(fmtstr, k, v.kind(), v.help())) + } + return strings.Join(entries, "\n") +} + +func appSpecRuneInvalid(r rune) bool { + if 'a' <= r && r <= 'z' { + return false + } + if 'A' <= r && r <= 'Z' { + return false + } + if '0' <= r && r <= '9' { + return false + } + if r == '-' || r == '+' || r == '_' { + return false + } + return true +} + +func appSpecStringInvalid(s string) error { + for _, r := range s { + if appSpecRuneInvalid(r) { + return fmt.Errorf("%s contains an invalid rune", strconv.Quote(s)) + } + } + return nil +} + +func appSpecHelpStringInvalid(s string) error { + if !printable(s) { + return fmt.Errorf("%s is not Unicode printable", strconv.Quote(s)) + } + return nil +} + +type appInteractProc struct { + Create bool `json:"create"` + OnCompletion string `json:"on-completion"` + Help string `json:"help"` + + Args []appInteractArg `json:"args"` + Accounts []appInteractAccount `json:"accounts"` + ForeignApps []appInteractForeign `json:"foreign"` +} + +func (proc appInteractProc) validate() (err error) { + err = appSpecStringInvalid(proc.OnCompletion) + if err != nil { + return fmt.Errorf("OnCompletion: %v", err) + } + err = appSpecHelpStringInvalid(proc.Help) + if err != nil { + return fmt.Errorf("Help: %v", err) + } + for i, arg := range proc.Args { + err = arg.validate() + if err != nil { + return fmt.Errorf("Arg(%d): %v", i, err) + } + } + for i, acc := range proc.Accounts { + err = acc.validate() + if err != nil { + return fmt.Errorf("Account(%d): %v", i, err) + } + } + for i, app := range proc.ForeignApps { + err = app.validate() + if err != nil { + return fmt.Errorf("App(%d): %v", i, err) + } + } + return +} + +func (proc appInteractProc) kind() string { + return proc.OnCompletion +} + +func (proc appInteractProc) help() string { + return proc.Help +} + +func (proc appInteractProc) pseudo() bool { + return false +} + +type appInteractArg struct { + Name string `json:"name"` + Kind string `json:"kind"` + Help string `json:"help"` + Pseudo bool `json:"pseudo"` + Default string `json:"default"` +} + +func (arg appInteractArg) validate() (err error) { + err = appSpecStringInvalid(arg.Name) + if err != nil { + return fmt.Errorf("Key: %v", err) + } + err = appSpecStringInvalid(arg.Kind) + if err != nil { + return fmt.Errorf("Kind: %v", err) + } + err = appSpecHelpStringInvalid(arg.Help) + if err != nil { + return fmt.Errorf("Help: %v", err) + } + // default values can be arbitrary + // make sure to escape them before printing! + // err = appSpecStringInvalid(arg.Default) + return +} + +func (arg appInteractArg) kind() string { + return arg.Kind +} + +func (arg appInteractArg) help() string { + return arg.Help +} + +func (arg appInteractArg) pseudo() bool { + return arg.Pseudo +} + +type appInteractAccount struct { + Name string `json:"name"` + Help string `json:"help"` + Pseudo bool `json:"pseudo"` // TODO use this field in help + Explicit bool `json:"explicit"` +} + +func (acc appInteractAccount) validate() (err error) { + err = appSpecStringInvalid(acc.Name) + if err != nil { + return fmt.Errorf("Name: %v", err) + } + err = appSpecHelpStringInvalid(acc.Help) + if err != nil { + return fmt.Errorf("Help: %v", err) + } + return +} + +type appInteractForeign struct { + Name string `json:"name"` + Help string `json:"help"` +} + +func (f appInteractForeign) validate() (err error) { + err = appSpecStringInvalid(f.Name) + if err != nil { + return fmt.Errorf("Name: %v", err) + } + err = appSpecHelpStringInvalid(f.Help) + if err != nil { + return fmt.Errorf("Help: %v", err) + } + return +} + +// map key -> data +type appInteractSchema map[string]appInteractSchemaEntry + +func (sch appInteractSchema) validate() (err error) { + for k, v := range sch { + err = appSpecStringInvalid(k) + if err != nil { + return fmt.Errorf("Key: %v", err) + } + err = v.validate() + if err != nil { + return fmt.Errorf("Entry(%s): %v", k, err) + } + } + return +} + +func (sch appInteractSchema) EntryList() string { + help := make(map[string]appInteractDatum) + for k, v := range sch { + help[k] = v + } + return helpList(help) +} + +func (sch appInteractSchema) EntryNames() (names []string) { + for k := range sch { + names = append(names, k) + } + return +} + +func (sch appInteractSchema) ToStateSchema() (schema basics.StateSchema) { + for name, entry := range sch { + size := uint64(1) + if entry.Map.Kind != "" { + if entry.Size < 0 { + reportErrorf("entry %s size %d < 0", name, entry.Size) + } + size = uint64(entry.Size) + } + switch entry.Kind { + case "int", "integer": + schema.NumUint += size + default: + schema.NumByteSlice += size + } + } + return +} + +type appInteractSchemaEntry struct { + Key string `json:"key"` + Kind string `json:"kind"` + Help string `json:"help"` + Size int `json:"size"` + Explicit bool `json:"explicit"` + + Map appInteractMap `json:"map"` // TODO support for queries +} + +func (entry appInteractSchemaEntry) validate() (err error) { + err = appSpecStringInvalid(entry.Key) + if err != nil { + return fmt.Errorf("Key: %v", err) + } + err = appSpecStringInvalid(entry.Kind) + if err != nil { + return fmt.Errorf("Kind: %v", err) + } + err = appSpecHelpStringInvalid(entry.Help) + if err != nil { + return fmt.Errorf("Help: %v", err) + } + err = entry.Map.validate() + if err != nil { + return fmt.Errorf("Map: %v", err) + } + return +} + +func (entry appInteractSchemaEntry) kind() string { + if entry.Map.Kind != "" { + return fmt.Sprintf("map %s -> %s", entry.Kind, entry.Map.Kind) + } + return entry.Kind +} + +func (entry appInteractSchemaEntry) help() string { + return entry.Help +} + +func (entry appInteractSchemaEntry) pseudo() bool { + return false +} + +type appInteractMap struct { + Kind string `json:"kind"` + Prefix string `json:"prefix"` +} + +func (m appInteractMap) validate() (err error) { + err = appSpecStringInvalid(m.Kind) + if err != nil { + return fmt.Errorf("Kind: %v", m.Kind) + } + err = appSpecStringInvalid(m.Prefix) + if err != nil { + return fmt.Errorf("Prefix: %v", m.Prefix) + } + return +} + +type appInteractState struct { + Global appInteractSchema `json:"global"` + Local appInteractSchema `json:"local"` +} + +func (s appInteractState) validate() (err error) { + err = s.Global.validate() + if err != nil { + return fmt.Errorf("Global: %v", err) + } + err = s.Local.validate() + if err != nil { + return fmt.Errorf("Local: %v", err) + } + return +} + +// map procedure name -> procedure +type appInteractProcs map[string]appInteractProc + +func (m appInteractProcs) validate() (err error) { + for k, v := range m { + err = appSpecStringInvalid(k) + if err != nil { + return fmt.Errorf("Key: %v", err) + } + err = v.validate() + if err != nil { + return fmt.Errorf("Proc(%s): %v", strconv.QuoteToASCII(k), err) + } + } + return +} + +type appInteractHeader struct { + Execute appInteractProcs `json:"execute"` + Query appInteractState `json:"query"` +} + +// TODO use reflect to recursively validate +func (hdr appInteractHeader) validate() (err error) { + err = hdr.Execute.validate() + if err != nil { + return fmt.Errorf("Execute: %v", err) + } + err = hdr.Query.validate() + if err != nil { + return fmt.Errorf("Query: %v", err) + } + return +} + +func (hdr appInteractHeader) ProcList() string { + help := make(map[string]appInteractDatum) + for k, v := range hdr.Execute { + help[k] = v + } + return helpList(help) +} + +func (hdr appInteractHeader) ProcNames() (names []string) { + for k := range hdr.Execute { + names = append(names, k) + } + return +} + +func parseAppHeader() (header appInteractHeader) { + if appHdr == "" { + reportErrorf("No header file provided") + } + + f, err := os.Open(appHdr) + if err != nil { + reportErrorf("Could not open app header file %s: %v", appHdr, err) + } + + dec := json.NewDecoder(f) + err = dec.Decode(&header) + if err != nil { + reportErrorf("Could not decode app header JSON file %s: %v", appHdr, err) + } + + err = header.validate() + if err != nil { + reportErrorf("App header JSON file could not validate: %v", err) + } + + return +} + +// TODO print help correctly when --help is passed but procedure/state name is given +// TODO complain when unknown flags are given + +var appExecuteCmd = &cobra.Command{ + Use: "execute", + Short: "Execute a procedure on an application", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + header := parseAppHeader() + proc, ok := header.Execute[args[0]] + if !ok { + reportErrorf("Unknown procedure name %s.\nDefined procedures in %s:\n%s", args[0], appHdr, header.ProcList()) + } + + if proc.Create != (appIdx == 0) { + reportErrorf("Procedure %s has the create flag set to %v, but application ID set to %d", args[0], proc.Create, appIdx) + } + + procFlags := cmd.Flags() + procFlags.SetInterspersed(true) + procArgs := make(map[string]interface{}) + procAccounts := make(map[string]*string) + procApps := make(map[string]*uint64) + for _, arg := range proc.Args { + switch arg.Kind { + case "int", "integer": + procArgs[arg.Name] = procFlags.Uint64(arg.Name, 0, arg.Help) + default: + procArgs[arg.Name] = procFlags.String(arg.Name, "", arg.Help) + } + } + for _, account := range proc.Accounts { + procAccounts[account.Name] = procFlags.String(account.Name, "", account.Help) + } + for _, app := range proc.ForeignApps { + procApps[app.Name] = procFlags.Uint64(app.Name, 0, app.Help) + } + procFlags.Parse(os.Args[1:]) + + var inputs appCallInputs + for _, arg := range proc.Args { + var callArg appCallArg + callArg.Encoding = arg.Kind + + if !procFlags.Changed(arg.Name) && arg.Default != "" { + callArg.Value = arg.Default + } else { + v := procArgs[arg.Name] + s, ok := v.(*string) + if ok { + callArg.Value = *s + } else { + i, ok := v.(*uint64) + if ok { + // TODO this decodes and re-encodes redundantly + callArg.Value = strconv.FormatUint(*i, 10) + } else { + reportErrorf("Could not re-encode key %s", arg.Name) + } + } + } + inputs.Args = append(inputs.Args, callArg) + } + for _, account := range proc.Accounts { + var addr basics.Address + s := *procAccounts[account.Name] + if s == "" { + if account.Explicit { + reportErrorf("Required account %s not provided", account.Name) + } + } else { + var err error + addr, err = basics.UnmarshalChecksumAddress(s) + if err != nil { + reportErrorf("Could not unmarshal address for %s (%s): %v", account.Name, s, err) + } + } + inputs.Accounts = append(inputs.Accounts, addr.String()) + } + for _, app := range proc.ForeignApps { + inputs.ForeignApps = append(inputs.ForeignApps, *procApps[app.Name]) + } + + if proc.OnCompletion == "" { + proc.OnCompletion = "NoOp" + } + onCompletion := mustParseOnCompletion(proc.OnCompletion) + appAccounts := inputs.Accounts + foreignApps := inputs.ForeignApps + + appArgs := make([][]byte, len(inputs.Args)) + for i, arg := range inputs.Args { + rawValue, err := parseAppArg(arg) + if err != nil { + reportErrorf("Could not parse argument corresponding to '%s': %v", proc.Args[i].Name, err) + } + appArgs[i] = rawValue + } + + if appIdx == 0 { + switch onCompletion { + case transactions.CloseOutOC, transactions.ClearStateOC: + reportWarnf("OnCompletion %s may be ill-formed when creating an application", onCompletion) + } + } + + var localSchema, globalSchema basics.StateSchema + var approvalProg, clearProg []byte + if appIdx == 0 { + approvalProg, clearProg = mustParseProgArgs() + localSchema = header.Query.Local.ToStateSchema() + globalSchema = header.Query.Global.ToStateSchema() + } + tx, err := client.MakeUnsignedApplicationCallTx(appIdx, appArgs, appAccounts, foreignApps, onCompletion, approvalProg, clearProg, globalSchema, localSchema) + if err != nil { + reportErrorf("Cannot create application txn: %v", err) + } + + // Fill in note and lease + tx.Note = parseNoteField(cmd) + tx.Lease = parseLease(cmd) + + // Fill in rounds, fee, etc. + fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) + if err != nil { + reportErrorf("Cannot determine last valid round: %s", err) + } + + tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) + if err != nil { + reportErrorf("Cannot construct transaction: %s", err) + } + + if outFilename == "" { + wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) + signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) + if err != nil { + reportErrorf(errorSigningTX, err) + } + + txid, err := client.BroadcastTransaction(signedTxn) + if err != nil { + reportErrorf(errorBroadcastingTX, err) + } + + if appIdx == 0 { + reportInfof("Attempting to create app (global ints %d, global blobs %d, local ints %d, local blobs %d, approval size %d, hash %v; clear size %d, hash %v)", globalSchema.NumUint, globalSchema.NumByteSlice, localSchema.NumUint, localSchema.NumByteSlice, len(approvalProg), crypto.HashObj(logic.Program(approvalProg)), len(clearProg), crypto.HashObj(logic.Program(clearProg))) + } else if onCompletion == transactions.UpdateApplicationOC { + reportInfof("Attempting to update app (approval size %d, hash %v; clear size %d, hash %v)", len(approvalProg), crypto.HashObj(logic.Program(approvalProg)), len(clearProg), crypto.HashObj(logic.Program(clearProg))) + } + reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) + + if !noWaitAfterSend { + err = waitForCommit(client, txid) + if err != nil { + reportErrorf(err.Error()) + } + // Check if we know about the transaction yet + txn, err := client.PendingTransactionInformation(txid) + if err != nil { + reportErrorf("%v", err) + } + if txn.TransactionResults != nil && txn.TransactionResults.CreatedAppIndex != 0 { + reportInfof("Created app with app index %d", txn.TransactionResults.CreatedAppIndex) + } + } + } else { + // Broadcast or write transaction to file + err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename) + if err != nil { + reportErrorf(err.Error()) + } + } + }, +} + +var appQueryCmd = &cobra.Command{ + Use: "query", + Short: "Query local or global state from an application", + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + header := parseAppHeader() + scope := "local" + storeName := account + lookup := header.Query.Local + if account == "" { + scope = "global" + storeName = "" + lookup = header.Query.Global + } + + param := args[0] + meta, ok := lookup[param] + if !ok { + reportErrorf("Unknown schema entry %s.\nDefined %s schema entries in %s:\n%s", param, scope, appHdr, lookup.EntryList()) + } + + var tealval basics.TealValue + if scope == "local" { + // Fetching local state. Get account information + ad, err := client.AccountData(account) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get application local state + local, ok := ad.AppLocalStates[basics.AppIndex(appIdx)] + if !ok { + reportErrorf(errorAccountNotOptedInToApp, account, appIdx) + } + + kv := local.KeyValue + tealval = kv[meta.Key] + } + + if scope == "global" { + // Fetching global state. Get application creator + app, err := client.ApplicationInformation(appIdx) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get creator information + ad, err := client.AccountData(app.Params.Creator) + if err != nil { + reportErrorf(errorRequestFail, err) + } + + // Get app params + params, ok := ad.AppParams[basics.AppIndex(appIdx)] + if !ok { + reportErrorf(errorNoSuchApplication, appIdx) + } + + kv := params.GlobalState + tealval = kv[meta.Key] + } + + var decoded string + switch meta.Kind { + case "int", "integer": + if tealval.Type == 0 { + if meta.Explicit { + reportErrorf("%s not set for %s.%s", param, appIdx, storeName) + } + } else if tealval.Type != basics.TealUintType { + reportErrorf("Expected kind %s but got teal type %s", meta.Kind, tealval.Type) + } + decoded = fmt.Sprintf("%d", tealval.Uint) + default: + if tealval.Type == 0 { + if meta.Explicit { + reportErrorf("%s not set for %s.%s", param, appIdx, storeName) + } + } else if tealval.Type != basics.TealBytesType { + reportErrorf("Expected kind %s but got teal type %s", meta.Kind, tealval.Type) + } + raw := []byte(tealval.Bytes) + switch meta.Kind { + case "str", "string": + decoded = tealval.Bytes + case "addr", "address": + var addr basics.Address + copy(addr[:], raw) + decoded = addr.String() + case "b32", "base32", "byte base32": + decoded = base32.StdEncoding.EncodeToString(raw) + case "b64", "base64", "byte base64": + decoded = base64.StdEncoding.EncodeToString(raw) + default: + decoded = tealval.Bytes + } + } + reportInfoln(decoded) + }, +} diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index 07a8f8093a..114b77d649 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -23,6 +23,7 @@ const ( errorRequestFail = "Error processing command: %s" errorGenesisIDFail = "Error determining kmd folder (%s). Ensure the node is running in %s." errorDirectoryNotExist = "Specified directory '%s' does not exist." + errorParseAddr = "Failed to parse addr: %v" // Account infoNoAccounts = "Did not find any account. Please import or create a new one." @@ -88,6 +89,15 @@ const ( // Asset malformedMetadataHash = "Cannot base64-decode metadata hash %s: %s" + // Application + errorLocalGlobal = "Exactly one of --local or --global is required" + errorLocalStateRequiresAccount = "--local requires --from account" + errorAccountNotOptedInToApp = "%s has not opted in to application %d" + errorNoSuchApplication = "application %d does not exist" + errorMarshalingState = "failed to encode state: %s" + errorApprovProgArgsRequired = "Exactly one of --approval-prog or --approval-prog-raw is required" + errorClearProgArgsRequired = "Exactly one of --clear-prog or --clear-prog-raw is required" + // Clerk infoTxIssued = "Sent %d MicroAlgos from account %s to address %s, transaction ID: %s. Fee set to %d" infoTxCommitted = "Transaction %s committed in round %d" diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go index 032fa76b08..778dfcbfb4 100644 --- a/cmd/opdoc/opdoc.go +++ b/cmd/opdoc/opdoc.go @@ -209,6 +209,15 @@ func argEnum(name string) []string { if name == "global" { return logic.GlobalFieldNames } + if name == "txna" || name == "gtxna" { + return logic.TxnaFieldNames + } + if name == "asset_holding_get" { + return logic.AssetHoldingFieldNames + } + if name == "asset_params_get" { + return logic.AssetParamsFieldNames + } return nil } @@ -241,6 +250,16 @@ func argEnumTypes(name string) string { if name == "global" { return typeString(logic.GlobalFieldTypes) } + if name == "txna" || name == "gtxna" { + return typeString(logic.TxnaFieldTypes) + } + if name == "asset_holding_get" { + return typeString(logic.AssetHoldingFieldTypes) + } + if name == "asset_params_get" { + return typeString(logic.AssetParamsFieldTypes) + } + return "" } diff --git a/cmd/pingpong/runCmd.go b/cmd/pingpong/runCmd.go index 3b07574741..d8fcc5ec57 100644 --- a/cmd/pingpong/runCmd.go +++ b/cmd/pingpong/runCmd.go @@ -57,6 +57,8 @@ var txnPerSec uint64 var teal string var groupSize uint32 var numAsset uint32 +var numApp uint32 +var appProgOps uint32 func init() { rootCmd.AddCommand(runCmd) @@ -86,6 +88,8 @@ func init() { runCmd.Flags().StringVar(&teal, "teal", "", "teal test scenario, can be light, normal, or heavy, this overrides --program") runCmd.Flags().Uint32Var(&groupSize, "groupsize", 1, "The number of transactions in each group") runCmd.Flags().Uint32Var(&numAsset, "numasset", 0, "The number of assets each account holds") + runCmd.Flags().Uint32Var(&numApp, "numapp", 0, "The number of apps each account opts in to") + runCmd.Flags().Uint32Var(&appProgOps, "appprogops", 0, "The approximate number of TEAL operations to perform in each ApplicationCall transaction") runCmd.Flags().BoolVar(&randomLease, "randomlease", false, "set the lease to contain a random value") } @@ -238,14 +242,26 @@ var runCmd = &cobra.Command{ if numAsset <= 1000 { cfg.NumAsset = numAsset } else { - reportErrorf("Invalid number of asset: %d, (valid number: 1 - 1000)\n", numAsset) + reportErrorf("Invalid number of assets: %d, (valid number: 0 - 1000)\n", numAsset) + } + + cfg.AppProgOps = appProgOps + + if numApp <= 1000 { + cfg.NumApp = numApp + } else { + reportErrorf("Invalid number of apps: %d, (valid number: 0 - 1000)\n", numApp) + } + + if numAsset != 0 && numApp != 0 { + reportErrorf("only one of numapp and numasset may be specified") } reportInfof("Preparing to initialize PingPong with config:\n") cfg.Dump(os.Stdout) // Initialize accounts if necessary - accounts, assetParams, cfg, err := pingpong.PrepareAccounts(ac, cfg) + accounts, assetParams, appParams, cfg, err := pingpong.PrepareAccounts(ac, cfg) if err != nil { reportErrorf("Error preparing accounts for transfers: %v\n", err) } @@ -258,7 +274,7 @@ var runCmd = &cobra.Command{ cfg.Dump(os.Stdout) // Kick off the real processing - pingpong.RunPingPong(context.Background(), ac, accounts, assetParams, cfg) + pingpong.RunPingPong(context.Background(), ac, accounts, assetParams, appParams, cfg) }, } diff --git a/cmd/tealdbg/README.md b/cmd/tealdbg/README.md new file mode 100644 index 0000000000..bac158b6ad --- /dev/null +++ b/cmd/tealdbg/README.md @@ -0,0 +1,268 @@ +# Algorand TEAL Debugger + +- [Algorand TEAL Debugger](#algorand-teal-debugger) + - [Quick Start](#quick-start) + - [Features Overview](#features-overview) + - [Local vs Remote Debugging](#local-vs-remote-debugging) + - [Frontends](#frontends) + - [Setting Execution Context](#setting-execution-context) + - [Protocol](#protocol) + - [Transaction and Transaction Group](#transaction-and-transaction-group) + - [Balance records](#balance-records) + - [Execution mode](#execution-mode) + - [Chrome DevTools Frontend Features](#chrome-devtools-frontend-features) + - [Configure the Listener](#configure-the-listener) + - [Supported Operations](#supported-operations) + - [Development and Architecture Overview](#development-and-architecture-overview) + - [TEAL Evaluator](#teal-evaluator) + - [Tealdbg](#tealdbg) + - [Debugger Core](#debugger-core) + - [Debug Adapter](#debug-adapter) + - [Algod hacking](#algod-hacking) + +## Quick Start + +1. Run the debugger + ``` + $ tealdbg debug myprog.teal + $ tealdbg debug samples/calls_count.teal --balance samples/calls_count_balance.json --txn samples/calls_count_txn.json --proto=future + $ tealdbg debug samples/calls_count.teal --proto=future --painless + ``` + It prints out the URL to follow: `chrome-devtools://devtools/bundled/js_app.html?...` +2. Open the URL in Google Chrome. + If you see the `This site can’t be reached` page, open Chrome DevTools [as explained](https://developers.google.com/web/tools/chrome-devtools/open) and refresh the page. + +## Features Overview + +### Local vs Remote Debugging + +The debugger runs either local programs or accept HTTP connections from remote evaluators +configured to run with a remote debugger hook. + +Local debugger allows debugging TEAL from files (both sources and compiled) or from transaction(s) +and balance records (see [Setting Debug Context](#setting-debug-context) for details). + +Remote debugger might be useful for debugging unit tests for TEAL (currently in Golang only) or for hacking **algod** `eval` and breaking on any TEAL evaluation. +The protocol consist of three REST endpoints and one data structure describing the evaluator state. +See `WebDebuggerHook` and `TestWebDebuggerManual` in [go-algorand sources](https://github.com/algorand/go-algorand/tree/master/data/transactions/logic) for more details. + +### Frontends + +Two frontends are available: + +1. Chrome DevTools (CDT): + ![CDT Screenshot](images/cdt-screenshot.png) +2. Web page + ![Web Page Screenshot](images/web-page-screenshot.png) + +## Setting Execution Context + +Local debugger supports setting the execution context: consensus protocol, transaction(s), balance records, execution mode. + +### Protocol + +Used to determine execution parameters and limits such as TEAL version, max program size and cost and so on. +``` +$ tealdbg debug --proto https://github.com/algorandfoundation/specs/tree/e5f565421d720c6f75cdd186f7098495caf9101f +$ tealdbg debug --proto future +``` + +### Transaction and Transaction Group + +Transaction(s) are used for: +1. Providing execution environment for TEAL programs. + Its fields are accessible by `txn`, `gtxn`, `txna` and `gtxna` instructions. +2. Retrieving TEAL code from `Lsig.Logic` or from `ApplicationCallTx` transactions. + +``` +$ tealdbg debug --txn samples/txn_group.json +``` + +If an array of transaction supplied then it is treated as a transaction group. To specify the current transaction for execution use `--group-index` option: + +``` +$ tealdbg debug --txn samples/txn_group.json --group-index=1 +``` + +Transaction(s) are JSON or MessagePack (`goal clerk` compatible) serialized instances of `transactions`. See [samples dir](https://github.com/algorand/go-algorand/tree/master/cmd/tealdbg/samples) for more examples. + +Sample transaction in JSON format: +```json +{ + "sig": "+FQBnfGQMNxzwW85WjpSKfOYoEKqzTChhJ+h2WYEx9C8Zt5THdKvHLd3IkPO/usubboFG/0Wcvb8C5Ps1h+IBQ==", + "txn": { + "amt": 1000, + "close": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", + "fee": 1176, + "fv": 12466, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "lv": 13466, + "note": "6gAVR0Nsv5Y=", + "rcv": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "pay" + } +} +``` + +### Balance records + +Balance records are used for setting environment for stateful TEAL execution and contains data available for TEAL with `balance`, `global Round`, `app_opted_in` and so on instructions. + +``` +$ tealdbg debug myprog.teal --balance samples/balances.json +``` + +Balance records are JSON or MessagePack (`goal account dump` compatible) serialized instances of `basics.BalanceRecord` + +Sample balance record in JSON format: +```json +{ + "addr": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "onl": 1, + "algo": 500000000, + "asset": { + "50": { + "a": 10 + } + }, + "appl": { + "100": { + "hsch": { + "nbs": 3, + "nui": 2 + }, + "tkv": { + "lkeybyte": { + "tb": "local", + "tt": 1 + }, + "lkeyint": { + "tt": 2, + "ui": 1 + } + } + } + } +} +``` + +If default/empty local and global state are OK for the application, the use `--painless` CLI option +to automatically create necessary balance records for the application(s) so that `app_` opcodes +do not fail due to absent data in ledger. + +### Execution mode + +Execution mode, either **signature** or **application** matches to **Algod**'s evaluation mode +for logic signature TEAL or application call TEAL. In short, determines either state access allowed or not with state access instructions. For example, `balance`, `global Round`, `app_opted_in` instructions are only available in **application** mode. + +``` +$ tealdbg debug myprog.teal --mode signature +``` + +Default value for `--mode` option is **auto** that forces the debugger to scan the program and to guess suitable execution mode. + +## Chrome DevTools Frontend Features + +### Configure the Listener + +Open `chrome://inspect/`, click **Configure**, type `localhost:9392` (default port) and save. +Then active CDT session will appear under "Remote Target" section. Click **inspect** to start debugging. + +Refer the screenshot for details: + +Chrome Inspect + +### Supported Operations + +1. **Resume** continues execution until next breakpoint if any. +2. **Step**, **Step Into**, **Step Over** are equivalents. +3. **Step Out** runs the program until the last instruction. +4. **Activate breakpoints** enables or disables all the breakpoints. +5. **Pause on Exceptions** enables breaking on evaluation error. +6. **Scope** pane allows examination of global fields, transaction object(s), + stack and scratch space. It also shows exception info if any. +7. **Breakpoints** pane shows active breakpoints. +8. **Line numbers** on the right allows breakpoints setting by a mouse-click. + +![CDT Screenshot](images/cdt-controls.png) + +Refer to the [Chrome DevTools debugging](https://developers.google.com/web/tools/chrome-devtools/javascript/reference) documentation for a complete guide. + + +## Development and Architecture Overview + +### TEAL Evaluator + +The evaluator accepts a new `Debugger` parameter described as the interface: +```golang +type DebuggerHook interface { + // Register is fired on program creation + Register(state *DebugState) error + // Update is fired on every step + Update(state *DebugState) error + // Complete is called when the program exits + Complete(state *DebugState) error +} +``` +If `Debugger` is set the evaluator calls `Register` on creation, `Update` on every step and `Complete` on exit. + +### Tealdbg + +The debugger consist of a core, transport adapters and debug adapters (frontends). + +### Debugger Core + +The core process `Register`, `Update` and `Complete` calls from the evaluator. +On `Register` it starts a new session and establish notification channel for state updates. +On `Update` it checks for breakpoints matches and if found, the debugger publishes the notification and waits for confirmation. +On `Complete` it publishes a final state update and removes the session. + +### Debug Adapter + +An adapter must implement the following interface: +```golang +type DebugAdapter interface { + SessionStarted(sid string, debugger Control, ch chan Notification) + SessionEnded(sid string) + WaitForCompletion() +} +``` + +The core calls `SessionStarted` for all adapters as part of dispatching `Register`. It is up to adapter to setup communication channel with a user. Then an adapter needs to start processing notifications from the channel and manage execution using debugger's `Control` interface. + +The core calls `SessionEnded` on `Complete` call. + +`WaitForCompletion` function might or might not be called by integrator (tealdbg). The main purpose is to prevent the main process termination when `evaluation` is done but the user is still working in UI. + +### Algod hacking + +**WARNING**: Use it only for private network for development purposes. + +If one needs to debug TEAL in as much real environment as possible then do + +1. Add `WebDebuggerHook` to `data/transactions/logic/eval.go`: + ```golang + cx.program = program + + // begin new code + debugURL := os.Getenv("TEAL_DEBUGGER_URL") + cx.Debugger = &WebDebuggerHook{URL: debugURL} + // end new code + + if cx.Debugger != nil { + ``` +2. Start the remote debugger + ``` + $ tealdbg remote + ``` +3. Rebuild algod + ``` + $ make install + ``` +4. Set `TEAL_DEBUGGER_URL` to debugger address and restart algod + ``` + $ export TEAL_DEBUGGER_URL=http://localhost:9392 + $ goal node restart + ``` diff --git a/cmd/tealdbg/bundle_home_html.sh b/cmd/tealdbg/bundle_home_html.sh new file mode 100755 index 0000000000..1706443a27 --- /dev/null +++ b/cmd/tealdbg/bundle_home_html.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +THISDIR=$(dirname $0) + +cat < $THISDIR/homepage.go +// 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 . + +// Code generated during build process. DO NOT EDIT. +package main + +var homepage string = \` +$(cat $THISDIR/home.html) +\` +EOM \ No newline at end of file diff --git a/cmd/tealdbg/cdtProto.go b/cmd/tealdbg/cdtProto.go new file mode 100644 index 0000000000..fea2bec17e --- /dev/null +++ b/cmd/tealdbg/cdtProto.go @@ -0,0 +1,253 @@ +// 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 . + +package main + +// definitions copied from packages below with json fields and struct names fixed +// "github.com/wirepair/gcd/gcdapi" +// "github.com/wirepair/gcd/gcdmessage" + +// ChromeResponse is default response object, contains the id and a result if applicable. +type ChromeResponse struct { + ID int64 `json:"id"` + Result interface{} `json:"result"` +} + +// ChromeRequest is default no-arg request +type ChromeRequest struct { + ID int64 `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params,omitempty"` +} + +type cdtTabDescription struct { + Description string `json:"description"` + DevtoolsFrontendURL string `json:"devtoolsFrontendUrl"` + ID string `json:"id"` + Title string `json:"title"` + TabType string `json:"type"` + URL string `json:"url"` + WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"` + DevtoolsFrontendURLCompat string `json:"devtoolsFrontendUrlCompat"` + FaviconURL string `json:"faviconUrl"` +} + +// RuntimeStackTraceID type +type RuntimeStackTraceID struct { + ID string `json:"id"` // + DebuggerID string `json:"debuggerId,omitempty"` // +} + +// RuntimeCallFrame type +type RuntimeCallFrame struct { + FunctionName string `json:"functionName"` // JavaScript function name. + ScriptID string `json:"scriptId"` // JavaScript script id. + URL string `json:"url"` // JavaScript script name or url. + LineNumber int `json:"lineNumber"` // JavaScript script line number (0-based). + ColumnNumber int `json:"columnNumber"` // JavaScript script column number (0-based). +} + +// RuntimeStackTrace type +type RuntimeStackTrace struct { + Description string `json:"description,omitempty"` // String label of this stack trace. For async traces this may be a name of the function that initiated the async call. + CallFrames []*RuntimeCallFrame `json:"callFrames"` // JavaScript function name. + Parent *RuntimeStackTrace `json:"parent,omitempty"` // Asynchronous JavaScript stack trace that preceded this stack, if available. + ParentID *RuntimeStackTraceID `json:"parentId,omitempty"` // Asynchronous JavaScript stack trace that preceded this stack, if available. +} + +// RuntimeExecutionContextDescription type +type RuntimeExecutionContextDescription struct { + ID int `json:"id"` // Unique id of the execution context. It can be used to specify in which execution context script evaluation should be performed. + Origin string `json:"origin"` // Execution context origin. + Name string `json:"name"` // Human readable name describing given context. + AuxData map[string]interface{} `json:"auxData,omitempty"` // Embedder-specific auxiliary data. +} + +// RuntimeExecutionContextCreatedParams type +type RuntimeExecutionContextCreatedParams struct { + Context RuntimeExecutionContextDescription `json:"context"` // A newly created execution context. +} + +// RuntimeExecutionContextCreatedEvent issued when new execution context is created. +type RuntimeExecutionContextCreatedEvent struct { + Method string `json:"method"` + Params RuntimeExecutionContextCreatedParams `json:"params,omitempty"` +} + +// RuntimeExecutionContextDestroyedParams type +type RuntimeExecutionContextDestroyedParams struct { + ExecutionContextID int `json:"executionContextId"` // Id of the destroyed context +} + +// RuntimeExecutionContextDestroyedEvent issued when execution context is destroyed. +type RuntimeExecutionContextDestroyedEvent struct { + Method string `json:"method"` + Params RuntimeExecutionContextDestroyedParams `json:"params,omitempty"` +} + +// RuntimeRemoteObject is mirror object referencing original JavaScript object. +type RuntimeRemoteObject struct { + Type string `json:"type"` // Object type. + Subtype string `json:"subtype,omitempty"` // Object subtype hint. Specified for `object` type values only. + ClassName string `json:"className,omitempty"` // Object class (constructor) name. Specified for `object` type values only. + Value interface{} `json:"value,omitempty"` // Remote object value in case of primitive values or JSON values (if it was requested). + UnserializableValue string `json:"unserializableValue,omitempty"` // Primitive value which can not be JSON-stringified does not have `value`, but gets this property. + Description string `json:"description,omitempty"` // String representation of the object. + ObjectID string `json:"objectId,omitempty"` // Unique object identifier (for non-primitive values). + Preview *RuntimeObjectPreview `json:"preview,omitempty"` // Preview containing abbreviated property values. Specified for `object` type values only. + CustomPreview *RuntimeCustomPreview `json:"customPreview,omitempty"` // +} + +// RuntimeCustomPreview type +type RuntimeCustomPreview struct { + Header string `json:"header"` // The JSON-stringified result of formatter.header(object, config) call. It contains json ML array that represents RemoteObject. + BodyGetterID string `json:"bodyGetterId,omitempty"` // If formatter returns true as a result of formatter.hasBody call then bodyGetterId will contain RemoteObjectId for the function that returns result of formatter.body(object, config) call. The result value is json ML array. +} + +// RuntimeObjectPreview is an object containing abbreviated remote object value. +type RuntimeObjectPreview struct { + Type string `json:"type"` // Object type. + Subtype string `json:"subtype,omitempty"` // Object subtype hint. Specified for `object` type values only. + Description string `json:"description,omitempty"` // String representation of the object. + Overflow bool `json:"overflow"` // True iff some of the properties or entries of the original object did not fit. + Properties []RuntimePropertyPreview `json:"properties"` // List of the properties. + Entries []RuntimeEntryPreview `json:"entries,omitempty"` // List of the entries. Specified for `map` and `set` subtype values only. +} + +// RuntimePropertyPreview type +type RuntimePropertyPreview struct { + Name string `json:"name"` // Property name. + Type string `json:"type"` // Object type. Accessor means that the property itself is an accessor property. + Value string `json:"value,omitempty"` // User-friendly property value string. + ValuePreview *RuntimeObjectPreview `json:"valuePreview,omitempty"` // Nested value preview. + Subtype string `json:"subtype,omitempty"` // Object subtype hint. Specified for `object` type values only. +} + +// RuntimeEntryPreview type +type RuntimeEntryPreview struct { + Key RuntimeObjectPreview `json:"key,omitempty"` // Preview of the key. Specified for map-like collection entries. + Value RuntimeObjectPreview `json:"value"` // Preview of the value. +} + +// RuntimePropertyDescriptor is object property descriptor. +type RuntimePropertyDescriptor struct { + Name string `json:"name"` // Property name or symbol description. + Value *RuntimeRemoteObject `json:"value,omitempty"` // The value associated with the property. + Writable bool `json:"writable"` // True if the value associated with the property may be changed (data descriptors only). + Get *RuntimeRemoteObject `json:"get,omitempty"` // A function which serves as a getter for the property, or `undefined` if there is no getter (accessor descriptors only). + Set *RuntimeRemoteObject `json:"set,omitempty"` // A function which serves as a setter for the property, or `undefined` if there is no setter (accessor descriptors only). + Configurable bool `json:"configurable"` // True if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object. + Enumerable bool `json:"enumerable"` // True if this property shows up during enumeration of the properties on the corresponding object. + WasThrown bool `json:"wasThrown,omitempty"` // True if the result was thrown during the evaluation. + IsOwn bool `json:"isOwn,omitempty"` // True if the property is owned for the object. + Symbol *RuntimeRemoteObject `json:"symbol,omitempty"` // Property symbol object, if the property is of the `symbol` type. +} + +// RuntimeCallArgument represents function call argument. Either remote object id `objectId`, primitive `value`, unserializable primitive value or neither of (for undefined) them should be specified. +type RuntimeCallArgument struct { + Value interface{} `json:"value,omitempty"` // Primitive value or serializable javascript object. + UnserializableValue string `json:"unserializableValue,omitempty"` // Primitive value which can not be JSON-stringified. + ObjectID string `json:"objectId,omitempty"` // Remote object handle. +} + +// RuntimeCallPackRangesObject is packRanges response object +type RuntimeCallPackRangesObject struct { + Type string `json:"type,omitempty"` + Value RuntimeCallPackRangesRange `json:"value,omitempty"` +} + +// RuntimeCallPackRangesRange range object +type RuntimeCallPackRangesRange struct { + Ranges [][3]int `json:"ranges,omitempty"` +} + +// DebuggerScriptParsedParams type +type DebuggerScriptParsedParams struct { + ScriptID string `json:"scriptId"` // Identifier of the script parsed. + URL string `json:"url"` // URL or name of the script parsed (if any). + StartLine int `json:"startLine"` // Line offset of the script within the resource with given URL (for script tags). + StartColumn int `json:"startColumn"` // Column offset of the script within the resource with given URL. + EndLine int `json:"endLine"` // Last line of the script. + EndColumn int `json:"endColumn"` // Length of the last line of the script. + ExecutionContextID int `json:"executionContextId"` // Specifies script creation context. + Hash string `json:"hash"` // Content hash of the script. + ExecutionContextAuxData map[string]interface{} `json:"executionContextAuxData,omitempty"` // Embedder-specific auxiliary data. + IsLiveEdit bool `json:"isLiveEdit,omitempty"` // True, if this script is generated as a result of the live edit operation. + SourceMapURL string `json:"sourceMapURL,omitempty"` // URL of source map associated with script (if any). + HasSourceURL bool `json:"hasSourceURL,omitempty"` // True, if this script has sourceURL. + IsModule bool `json:"isModule,omitempty"` // True, if this script is ES6 module. + Length int `json:"length,omitempty"` // This script length. + StackTrace RuntimeStackTrace `json:"stackTrace,omitempty"` // JavaScript top stack frame of where the script parsed event was triggered if available. +} + +// DebuggerScriptParsedEvent type +type DebuggerScriptParsedEvent struct { + Method string `json:"method"` + Params DebuggerScriptParsedParams `json:"params,omitempty"` +} + +// DebuggerLocation is location in the source code. +type DebuggerLocation struct { + ScriptID string `json:"scriptId"` // Script identifier as reported in the `Debugger.scriptParsed`. + LineNumber int `json:"lineNumber"` // Line number in the script (0-based). + ColumnNumber int `json:"columnNumber"` // Column number in the script (0-based). +} + +// DebuggerContinueToLocationParams type +type DebuggerContinueToLocationParams struct { + // Location to continue to. + Location DebuggerLocation `json:"location"` + // + TargetCallFrames string `json:"targetCallFrames,omitempty"` +} + +// DebuggerCallFrame is JavaScript call frame. Array of call frames form the call stack. +type DebuggerCallFrame struct { + CallFrameID string `json:"callFrameId"` // Call frame identifier. This identifier is only valid while the virtual machine is paused. + FunctionName string `json:"functionName"` // Name of the JavaScript function called on this call frame. + FunctionLocation *DebuggerLocation `json:"functionLocation,omitempty"` // Location in the source code. + Location *DebuggerLocation `json:"location"` // Location in the source code. + URL string `json:"url"` // JavaScript script name or url. + ScopeChain []DebuggerScope `json:"scopeChain"` // Scope chain for this call frame. + This *RuntimeRemoteObject `json:"this"` // `this` object for this call frame. + ReturnValue *RuntimeRemoteObject `json:"returnValue,omitempty"` // The value being returned, if the function is at return point. +} + +// DebuggerScope description. +type DebuggerScope struct { + Type string `json:"type"` // Scope type. + Object RuntimeRemoteObject `json:"object"` // Object representing the scope. For `global` and `with` scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. + Name string `json:"name,omitempty"` // + StartLocation *DebuggerLocation `json:"startLocation,omitempty"` // Location in the source code where scope starts + EndLocation *DebuggerLocation `json:"endLocation,omitempty"` // Location in the source code where scope ends +} + +// DebuggerPausedParams type +type DebuggerPausedParams struct { + CallFrames []DebuggerCallFrame `json:"callFrames"` // Call stack the virtual machine stopped on. + Reason string `json:"reason"` // Pause reason. + Data map[string]interface{} `json:"data,omitempty"` // Object containing break-specific auxiliary properties. + HitBreakpoints []string `json:"hitBreakpoints"` // Hit breakpoints IDs + AsyncStackTrace *RuntimeStackTrace `json:"asyncStackTrace,omitempty"` // Async stack trace, if any. + AsyncStackTraceID *RuntimeStackTraceID `json:"asyncStackTraceId,omitempty"` // Async stack trace, if any. + AsyncCallStackTraceID *RuntimeStackTraceID `json:"asyncCallStackTraceId,omitempty"` // Just scheduled async call will have this stack trace as parent stack during async execution. This field is available only after `Debugger.stepInto` call with `breakOnAsynCall` flag. +} + +// DebuggerPausedEvent is fired when the virtual machine stopped on breakpoint or exception or any other stop criteria. +type DebuggerPausedEvent struct { + Method string `json:"method"` + Params DebuggerPausedParams `json:"params,omitempty"` +} diff --git a/cmd/tealdbg/cdtSession.go b/cmd/tealdbg/cdtSession.go new file mode 100644 index 0000000000..0a1d84fe3b --- /dev/null +++ b/cmd/tealdbg/cdtSession.go @@ -0,0 +1,630 @@ +// 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 . + +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net/http" + "strconv" + "strings" + "sync/atomic" + + "github.com/algorand/go-deadlock" + "github.com/algorand/websocket" + + "github.com/algorand/go-algorand/data/transactions/logic" +) + +type cdtSession struct { + uuid string + debugger Control + notifications chan Notification + endpoint cdtTabDescription + done chan struct{} + + contextID int + scriptID string + scriptHash string + scriptURL string + sourceMapURL string + states appState + + verbose bool +} + +var contextCounter int32 = 0 +var scriptCounter int32 = 0 + +func makeCDTSession(uuid string, debugger Control, ch chan Notification) *cdtSession { + s := new(cdtSession) + s.uuid = uuid + s.debugger = debugger + s.notifications = ch + s.done = make(chan struct{}) + s.contextID = int(atomic.AddInt32(&contextCounter, 1)) + s.scriptID = strconv.Itoa(int(atomic.AddInt32(&scriptCounter, 1))) + return s +} + +func (s *cdtSession) sourceMapHandler(w http.ResponseWriter, r *http.Request) { + sm, err := s.debugger.GetSourceMap() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + w.WriteHeader(http.StatusOK) + w.Write(sm) + return +} + +func (s *cdtSession) sourceHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, source := s.debugger.GetSource() + w.Write(source) + return +} + +func (s *cdtSession) websocketHandler(w http.ResponseWriter, r *http.Request) { + defer func() { + close(s.done) + }() + + uuid := r.URL.Path + if uuid[0] == '/' { + uuid = uuid[1:] + } + if uuid != s.uuid { + w.WriteHeader(http.StatusForbidden) + return + } + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Error on connection upgrade: %v\n", err) + return + } + defer ws.Close() + + notifications := s.notifications + + cdtRespCh := make(chan ChromeResponse, 128) + cdtEventCh := make(chan interface{}, 128) + cdtUpdatedCh := make(chan interface{}, 1) + + closed := make(chan struct{}) + registered := make(chan struct{}) + + var dbgStateMu deadlock.Mutex + var dbgState logic.DebugState + + var state cdtState + + // Debugger notifications processing loop + go func() { + for { + select { + case notification := <-notifications: + if s.verbose { + log.Printf("received: %s\n", notification.Event) + } + + switch notification.Event { + case "registered": + // no mutex, the access already synchronized by "registered" chan + dbgState = notification.DebugState + registered <- struct{}{} + case "completed": + // if completed we still want to see updated state + state.completed.SetTo(true) + close(notifications) + fallthrough + case "updated": + dbgStateMu.Lock() + dbgState = notification.DebugState + dbgStateMu.Unlock() + cdtUpdatedCh <- struct{}{} + default: + log.Println("Unk event: " + notification.Event) + } + case <-closed: + return + } + if state.completed.IsSet() { + return + } + } + }() + + // wait until initial "registered" event + <-registered + + func() { + dbgStateMu.Lock() + defer dbgStateMu.Unlock() + + // set immutable items + state.Init(dbgState.Disassembly, dbgState.Proto, dbgState.TxnGroup, dbgState.GroupIndex, dbgState.Globals) + // mutable + // set pc and line to 0 to workaround Register ack + state.Update(cdtStateUpdate{ + dbgState.Stack, dbgState.Scratch, + 0, 0, "", + s.debugger.GetStates(nil), + }) + + hash := sha256.Sum256([]byte(state.disassembly)) // some random hash + s.scriptHash = hex.EncodeToString(hash[:]) + s.scriptURL = fmt.Sprintf("file://%s.teal", s.scriptHash) // some random name if not specified + }() + + // Chrome Devtools reader + go func() { + for { + var cdtReq ChromeRequest + mtype, reader, err := ws.NextReader() + if err != nil { + closed <- struct{}{} + close(closed) + return + } + if mtype != websocket.TextMessage { + log.Printf("Unexpected type: %d\n", mtype) + continue + } + msg := make([]byte, 64000) + n, err := reader.Read(msg) + if err != nil { + closed <- struct{}{} + close(closed) + return + } + json.Unmarshal(msg[:n], &cdtReq) + + if s.verbose { + log.Printf("%v\n", cdtReq) + } + + dbgStateMu.Lock() + cdtResp, events, err := s.handleCDTRequest(&cdtReq, &state) + dbgStateMu.Unlock() + if err != nil { + log.Println(err.Error()) + continue + } + cdtRespCh <- cdtResp + for _, event := range events { + cdtEventCh <- event + } + } + }() + + // Chrome Devtools writer + go func() { + for { + select { + case devtoolResp := <-cdtRespCh: + if s.verbose { + log.Printf("responsing: %v\n", devtoolResp) + } + err := ws.WriteJSON(&devtoolResp) + if err != nil { + log.Println(err.Error()) + return + } + case devtoolEv := <-cdtEventCh: + if s.verbose { + log.Printf("firing: %v\n", devtoolEv) + } + err := ws.WriteJSON(&devtoolEv) + if err != nil { + log.Println(err.Error()) + return + } + case <-cdtUpdatedCh: + dbgStateMu.Lock() + + appState := s.debugger.GetStates(&dbgState.AppStateChage) + state.Update(cdtStateUpdate{ + dbgState.Stack, dbgState.Scratch, + dbgState.PC, dbgState.Line, dbgState.Error, + appState, + }) + dbgStateMu.Unlock() + + event := s.computeEvent(&state) + cdtEventCh <- event + case <-closed: + return + } + } + }() + + <-closed + + // handle CDT window closing without resuming execution + // resume and consume a final "completed" notification + if !state.completed.IsSet() { + s.debugger.SetBreakpointsActive(false) + s.debugger.Resume() + defer func() { + for { + select { + case <-notifications: + return + } + } + }() + } +} + +func (s *cdtSession) handleCDTRequest(req *ChromeRequest, state *cdtState) (response ChromeResponse, events []interface{}, err error) { + empty := make(map[string]interface{}) + type cmdResult struct { + Result interface{} `json:"result"` + } + switch req.Method { + case "Debugger.enable": + evCtxCreated := s.makeContextCreatedEvent() + evParsed := s.makeScriptParsedEvent(state) + events = append(events, &evCtxCreated, &evParsed) + + debuggerID := make(map[string]string) + debuggerID["debuggerId"] = s.uuid + response = ChromeResponse{ID: req.ID, Result: debuggerID} + case "Runtime.runIfWaitingForDebugger": + evPaused := s.makeDebuggerPausedEvent(state) + events = append(events, &evPaused) + response = ChromeResponse{ID: req.ID, Result: empty} + case "Runtime.getIsolateId": + isolateID := make(map[string]string) + isolateID["id"] = s.uuid + response = ChromeResponse{ID: req.ID, Result: isolateID} + case "Debugger.getScriptSource": + p := req.Params.(map[string]interface{}) + _, ok := p["scriptId"] + source := make(map[string]string) + if !ok { + err = fmt.Errorf("getScriptSource failed: no scriptId") + return + } + source["scriptSource"] = state.disassembly + response = ChromeResponse{ID: req.ID, Result: source} + case "Debugger.setPauseOnExceptions": + p := req.Params.(map[string]interface{}) + stateRaw, ok := p["state"] + enable := false + if ok { + if state, ok := stateRaw.(string); ok && state != "none" { + enable = true + } + } + state.pauseOnError.SetTo(enable) + response = ChromeResponse{ID: req.ID, Result: empty} + case "Runtime.evaluate": + p := req.Params.(map[string]interface{}) + exprRaw, ok := p["expression"] + if !ok { + err = fmt.Errorf("evaluate failed: no expression") + return + } + + expr := exprRaw.(string) + if expr == "navigator.userAgent" { + obj := makeStringResult("Algorand TEAL Debugger") + response = ChromeResponse{ID: req.ID, Result: cmdResult{obj}} + } else { + response = ChromeResponse{ID: req.ID, Result: cmdResult{}} + } + case "Runtime.callFunctionOn": + p := req.Params.(map[string]interface{}) + objIDRaw, ok := p["objectId"] + if !ok { + err = fmt.Errorf("callFunctionOn failed: no objectId") + return + } + objID := objIDRaw.(string) + funcDeclRaw, ok := p["functionDeclaration"] + if !ok { + err = fmt.Errorf("callFunctionOn failed: no functionDeclaration") + return + } + funcDecl := funcDeclRaw.(string) + argsRaw, ok := p["arguments"] + if !ok { + err = fmt.Errorf("callFunctionOn failed: no arguments") + return + } + args := argsRaw.([]interface{}) + if strings.HasPrefix(funcDecl, "function packRanges") { + ranges := state.packRanges(objID, args) + response = ChromeResponse{ID: req.ID, Result: cmdResult{ranges}} + } else if strings.HasPrefix(funcDecl, "function buildArrayFragment") || strings.HasPrefix(funcDecl, "function buildObjectFragment") { + obj := state.buildFragment(objID, args) + response = ChromeResponse{ID: req.ID, Result: cmdResult{obj}} + } else { + response = ChromeResponse{ID: req.ID, Result: cmdResult{}} + } + case "Runtime.getProperties": + p := req.Params.(map[string]interface{}) + objIDRaw, ok := p["objectId"] + if !ok { + err = fmt.Errorf("getProperties failed: no objectId") + return + } + objID := objIDRaw.(string) + + preview := false + previewRaw, ok := p["generatePreview"] + if ok { + preview = previewRaw.(bool) + } + + var desc []RuntimePropertyDescriptor + desc, err = state.getObjectDescriptor(objID, preview) + if err != nil { + err = fmt.Errorf("getObjectDescriptor error: " + err.Error()) + return + } + + if s.verbose { + var data []byte + data, err = json.Marshal(desc) + if err != nil { + err = fmt.Errorf("getObjectDescriptor json error: " + err.Error()) + return + } + log.Printf("Desc object: %s", string(data)) + } + + response = ChromeResponse{ID: req.ID, Result: cmdResult{desc}} + case "Debugger.setBreakpointsActive": + p := req.Params.(map[string]interface{}) + activeRaw, ok := p["active"] + active := false + if ok { + if value, ok := activeRaw.(bool); ok && value { + active = true + } + } + s.debugger.SetBreakpointsActive(active) + + response = ChromeResponse{ID: req.ID, Result: empty} + case "Debugger.getPossibleBreakpoints": + p := req.Params.(map[string]interface{}) + var start, end map[string]interface{} + var startLine, endLine int + var scriptID string + if _, ok := p["start"]; !ok { + response = ChromeResponse{ID: req.ID, Result: empty} + return + } + + start = p["start"].(map[string]interface{}) + startLine = int(start["lineNumber"].(float64)) + scriptID = start["scriptId"].(string) + if _, ok := p["end"]; ok { + end = p["end"].(map[string]interface{}) + endLine = int(end["lineNumber"].(float64)) + } else { + endLine = startLine + } + + result := make(map[string]interface{}) + locs := make([]DebuggerLocation, 0, endLine-startLine+1) + for ln := startLine; ln < endLine; ln++ { + locs = append(locs, DebuggerLocation{ScriptID: scriptID, LineNumber: ln}) + } + result["locations"] = locs + response = ChromeResponse{ID: req.ID, Result: result} + case "Debugger.removeBreakpoint": + p := req.Params.(map[string]interface{}) + var bpLine int + bpLine, err = strconv.Atoi(p["breakpointId"].(string)) + if err != nil { + return + } + err = s.debugger.RemoveBreakpoint(bpLine) + if err != nil { + return + } + response = ChromeResponse{ID: req.ID, Result: empty} + case "Debugger.setBreakpointByUrl": + p := req.Params.(map[string]interface{}) + bpLine := int(p["lineNumber"].(float64)) + err = s.debugger.SetBreakpoint(bpLine) + if err != nil { + return + } + + result := make(map[string]interface{}) + result["breakpointId"] = strconv.Itoa(bpLine) + result["locations"] = []DebuggerLocation{ + {ScriptID: s.scriptID, LineNumber: bpLine}, + } + response = ChromeResponse{ID: req.ID, Result: result} + case "Debugger.resume": + state.lastAction.Store("resume") + s.debugger.Resume() + if state.completed.IsSet() { + evDestroyed := s.makeContextDestroyedEvent() + events = append(events, &evDestroyed) + } + response = ChromeResponse{ID: req.ID, Result: empty} + case "Debugger.stepOut": + state.lastAction.Store("step") + state.pauseOnCompeted.SetTo(true) + s.debugger.Resume() + if state.completed.IsSet() { + evDestroyed := s.makeContextDestroyedEvent() + events = append(events, &evDestroyed) + } + response = ChromeResponse{ID: req.ID, Result: empty} + case "Debugger.stepOver", "Debugger.stepInto": + state.lastAction.Store("step") + s.debugger.Step() + if state.completed.IsSet() { + evDestroyed := s.makeContextDestroyedEvent() + events = append(events, &evDestroyed) + } + response = ChromeResponse{ID: req.ID, Result: empty} + default: + response = ChromeResponse{ID: req.ID, Result: empty} + } + + return +} + +func (s *cdtSession) computeEvent(state *cdtState) (event interface{}) { + if state.completed.IsSet() { + if state.pauseOnCompeted.IsSet() { + event = s.makeDebuggerPausedEvent(state) + return + } + if state.pauseOnError.IsSet() && state.err.Length() != 0 { + event = s.makeDebuggerPausedEvent(state) + return + } + if state.lastAction.Load() == "resume" { + event = s.makeContextDestroyedEvent() + return + } + } + + event = s.makeDebuggerPausedEvent(state) + return +} + +func (s *cdtSession) makeScriptParsedEvent(state *cdtState) DebuggerScriptParsedEvent { + // {"method":"Debugger.scriptParsed","params":{"scriptId":"69","url":"internal/dtrace.js","startLine":0,"startColumn":0,"endLine":21,"endColumn":0,"executionContextId":1,"hash":"2e8fbf2f9f6aaa183be557d25f5fbc5b09fae00a","executionContextAuxData":{"isDefault":true},"isLiveEdit":false,"sourceMapURL":"","hasSourceURL":false,"isModule":false,"length":568,"stackTrace":{"callFrames":[{"functionName":"NativeModule.compile","scriptId":"7","url":"internal/bootstrap/loaders.js","lineNumber":298,"columnNumber":15}]}}} + progLines := strings.Count(state.disassembly, "\n") + length := len(state.disassembly) + + evParsed := DebuggerScriptParsedEvent{ + Method: "Debugger.scriptParsed", + Params: DebuggerScriptParsedParams{ + ScriptID: s.scriptID, + URL: s.scriptURL, + SourceMapURL: s.sourceMapURL, + StartLine: 0, + StartColumn: 0, + EndLine: progLines, + EndColumn: 0, + ExecutionContextID: s.contextID, + Hash: s.scriptHash, + IsLiveEdit: false, + Length: length, + }, + } + return evParsed +} + +func (s *cdtSession) makeDebuggerPausedEvent(state *cdtState) DebuggerPausedEvent { + progLines := strings.Count(state.disassembly, "\n") + + scopeLocal := DebuggerScope{ + Type: "local", + Object: RuntimeRemoteObject{ + Type: "object", + ClassName: "Object", + Description: "Object", + ObjectID: localScopeObjID, + }, + StartLocation: &DebuggerLocation{ + ScriptID: s.scriptID, + LineNumber: 0, + ColumnNumber: 0, + }, + EndLocation: &DebuggerLocation{ + ScriptID: s.scriptID, + LineNumber: progLines, + ColumnNumber: 0, + }, + } + scopeGlobal := DebuggerScope{ + Type: "global", + Object: RuntimeRemoteObject{ + Type: "object", + ClassName: "Object", + Description: "Object", + ObjectID: globalScopeObjID, + }, + } + sc := []DebuggerScope{scopeLocal, scopeGlobal} + cf := DebuggerCallFrame{ + CallFrameID: "mainframe", + FunctionName: "", + Location: &DebuggerLocation{ + ScriptID: s.scriptID, + LineNumber: state.line.Load(), + ColumnNumber: 0, + }, + URL: s.scriptURL, + ScopeChain: sc, + } + + evPaused := DebuggerPausedEvent{ + Method: "Debugger.paused", + Params: DebuggerPausedParams{ + CallFrames: []DebuggerCallFrame{cf}, + Reason: "other", + HitBreakpoints: make([]string, 0), + }, + } + + if lastError := state.err.Load(); len(lastError) != 0 { + evPaused.Params.Reason = "exception" + evPaused.Params.Data = map[string]interface{}{ + "type": "object", + "className": "Error", + "description": lastError, + "objectId": "tealErrorID", + } + } + + return evPaused +} + +func (s *cdtSession) makeContextCreatedEvent() RuntimeExecutionContextCreatedEvent { + // {"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"origin":"","name":"node[47576]","auxData":{"isDefault":true}}}} + + aux := make(map[string]interface{}) + aux["isDefault"] = true + evCtxCreated := RuntimeExecutionContextCreatedEvent{ + Method: "Runtime.executionContextCreated", + Params: RuntimeExecutionContextCreatedParams{ + Context: RuntimeExecutionContextDescription{ + ID: s.contextID, + Origin: "", + Name: "TEAL program", + AuxData: map[string]interface{}{"isDefault": true}, + }, + }, + } + return evCtxCreated +} + +func (s *cdtSession) makeContextDestroyedEvent() RuntimeExecutionContextDestroyedEvent { + return RuntimeExecutionContextDestroyedEvent{ + Method: "Runtime.executionContextDestroyed", + Params: RuntimeExecutionContextDestroyedParams{s.contextID}, + } +} diff --git a/cmd/tealdbg/cdtState.go b/cmd/tealdbg/cdtState.go new file mode 100644 index 0000000000..f78a3c3251 --- /dev/null +++ b/cmd/tealdbg/cdtState.go @@ -0,0 +1,786 @@ +// 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 . + +package main + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "math" + "strconv" + "strings" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" +) + +type cdtState struct { + // immutable content + disassembly string + proto *config.ConsensusParams + txnGroup []transactions.SignedTxn + groupIndex int + globals []basics.TealValue + + // mutable program state + mu deadlock.Mutex + stack []basics.TealValue + scratch []basics.TealValue + pc atomicInt + line atomicInt + err atomicString + appState + + // debugger states + lastAction atomicString + pauseOnError atomicBool + pauseOnCompeted atomicBool + completed atomicBool +} + +type cdtStateUpdate struct { + stack []basics.TealValue + scratch []basics.TealValue + pc int + line int + err string + + appState +} + +func (s *cdtState) Init(disassembly string, proto *config.ConsensusParams, txnGroup []transactions.SignedTxn, groupIndex int, globals []basics.TealValue) { + s.disassembly = disassembly + s.proto = proto + s.txnGroup = txnGroup + s.groupIndex = groupIndex + s.globals = globals +} + +func (s *cdtState) Update(state cdtStateUpdate) { + s.mu.Lock() + defer s.mu.Unlock() + s.pc.Store(state.pc) + s.line.Store(state.line) + s.err.Store(state.err) + s.stack = state.stack + s.scratch = state.scratch + s.appState = state.appState +} + +const localScopeObjID = "localScopeObjId" +const globalScopeObjID = "globalScopeObjID" +const globalsObjID = "globalsObjID" +const txnObjID = "txnObjID" +const gtxnObjID = "gtxnObjID" +const stackObjID = "stackObjID" +const scratchObjID = "scratchObjID" +const tealErrorID = "tealErrorID" +const appGlobalObjID = "appGlobalObjID" +const appLocalsObjID = "appLocalsObjID" + +type objectDescFn func(s *cdtState, preview bool) []RuntimePropertyDescriptor + +var objectDescMap = map[string]objectDescFn{ + globalScopeObjID: makeGlobalScope, + localScopeObjID: makeLocalScope, + globalsObjID: makeGlobals, + txnObjID: makeTxn, + gtxnObjID: makeTxnGroup, + stackObjID: makeStack, + scratchObjID: makeScratch, + tealErrorID: makeTealError, + appGlobalObjID: makeAppGlobalState, + appLocalsObjID: makeAppLocalsState, +} + +func (s *cdtState) getObjectDescriptor(objID string, preview bool) (desc []RuntimePropertyDescriptor, err error) { + maker, ok := objectDescMap[objID] + if !ok { + if idx, ok := decodeGroupTxnID(objID); ok { + if idx >= len(s.txnGroup) || idx < 0 { + err = fmt.Errorf("invalid group idx: %d", idx) + return + } + if len(s.txnGroup) > 0 { + return makeTxnImpl(&s.txnGroup[idx].Txn, idx, preview), nil + } + } else if parentObjID, ok := decodeArrayLength(objID); ok { + switch parentObjID { + case stackObjID: + return makeArrayLength(s.stack), nil + case scratchObjID: + return makeArrayLength(s.scratch), nil + default: + } + } else if parentObjID, from, to, ok := decodeArraySlice(objID); ok { + switch parentObjID { + case stackObjID: + return makeStackSlice(s, from, to, preview), nil + case scratchObjID: + return makeScratchSlice(s, from, to, preview), nil + default: + } + } else if appID, ok := decodeAppGlobalAppID(objID); ok { + return makeAppGlobalKV(s, appID), nil + } else if addr, appID, ok := decodeAppLocalsAppID(objID); ok { + return makeAppLocalsKV(s, addr, appID), nil + } else if addr, ok := decodeAppLocalsAddr(objID); ok { + return makeAppLocalState(s, addr), nil + } + // might be nested object in array, parse and call + err = fmt.Errorf("unk object id: %s", objID) + return + } + return maker(s, preview), nil +} + +func convertCallArgs(argsRaw []interface{}) (args []RuntimeCallArgument) { + for _, item := range argsRaw { + argRaw := item.(map[string]interface{}) + value := argRaw["value"] + args = append(args, RuntimeCallArgument{Value: value}) + } + return +} + +func (s *cdtState) packRanges(objID string, argsRaw []interface{}) (result RuntimeCallPackRangesObject) { + if len(argsRaw) < 5 { + return + } + + args := convertCallArgs(argsRaw) + fromIndex := int(args[0].Value.(float64)) + toIndex := int(args[1].Value.(float64)) + bucketThreshold := int(args[2].Value.(float64)) + // sparseIterationThreshold := args[3].Value.(float64) + // getOwnPropertyNamesThreshold := args[4].Value.(float64) + + // based on JS code that CDT asks to execute + count := toIndex - fromIndex + 1 + bucketSize := count + if count > bucketThreshold { + bucketSize = int(math.Pow(float64(bucketThreshold), math.Ceil(math.Log(float64(count))/math.Log(float64(bucketThreshold)))-1)) + } + + var ranges [][3]int + + count = 0 + groupStart := -1 + groupEnd := 0 + for i := fromIndex; i <= toIndex; i++ { + if groupStart == -1 { + groupStart = i + } + groupEnd = i + count++ + if count == bucketSize { + ranges = append(ranges, [3]int{groupStart, groupEnd, count}) + count = 0 + groupStart = -1 + } + } + if count > 0 { + ranges = append(ranges, [3]int{groupStart, groupEnd, count}) + } + + result.Type = "object" + result.Value = RuntimeCallPackRangesRange{ + Ranges: ranges, + } + + return +} + +func (s *cdtState) buildFragment(objID string, argsRaw []interface{}) RuntimeRemoteObject { + var source []basics.TealValue + switch objID { + case stackObjID: + source = s.stack + case scratchObjID: + source = s.scratch + default: + return RuntimeRemoteObject{} + } + + // buildObjectFragment + if len(argsRaw) < 3 { + return RuntimeRemoteObject{ + Type: "object", + Subtype: "array", + ClassName: "Array", + Description: fmt.Sprintf("Array(%d)", len(source)), + ObjectID: encodeArrayLength(objID), + } + } + + // buildArrayFragment + + args := convertCallArgs(argsRaw) + fromIndex := int(args[0].Value.(float64)) + toIndex := int(args[1].Value.(float64)) + // sparseIterationThreshold := args[2].Value.(float64) + + return RuntimeRemoteObject{ + Type: "object", + ClassName: "Object", + Description: "Object", + ObjectID: encodeArraySlice(objID, fromIndex, toIndex), + } +} + +func makeObject(name, id string) RuntimePropertyDescriptor { + return RuntimePropertyDescriptor{ + Name: name, + Configurable: false, + Writable: false, + Enumerable: true, + IsOwn: true, + Value: &RuntimeRemoteObject{ + Type: "object", + ClassName: "Object", + Description: "Object", + ObjectID: id, + }, + } +} + +func makeArray(name string, length int, id string) RuntimePropertyDescriptor { + return RuntimePropertyDescriptor{ + Name: name, + Configurable: false, + Writable: false, + Enumerable: true, + IsOwn: true, + Value: &RuntimeRemoteObject{ + Type: "object", + Subtype: "array", + ClassName: "Array", + Description: fmt.Sprintf("Array(%d)", length), + ObjectID: id, + }, + } +} + +func makePrimitive(field fieldDesc) RuntimePropertyDescriptor { + return RuntimePropertyDescriptor{ + Name: field.Name, + Configurable: false, + Writable: false, + Enumerable: true, + IsOwn: true, + Value: &RuntimeRemoteObject{ + Type: field.Type, + Value: field.Value, + }, + } +} + +func makeStringResult(value string) RuntimeRemoteObject { + return RuntimeRemoteObject{ + Type: "string", + Value: value, + } +} + +// tealTypeMap maps TealType to JS type +var tealTypeMap = map[basics.TealType]string{ + basics.TealBytesType: "string", + basics.TealUintType: "number", +} + +type fieldDesc struct { + Name string + Value string + Type string +} + +func prepareGlobals(globals []basics.TealValue) []fieldDesc { + result := make([]fieldDesc, 0, len(logic.GlobalFieldNames)) + if len(globals) != len(logic.GlobalFieldNames) { + desc := fieldDesc{ + "error", + fmt.Sprintf("globals: invalid length %d != %d", len(globals), len(logic.GlobalFieldNames)), + "undefined", + } + result = append(result, desc) + return result + } + + for fieldIdx, name := range logic.GlobalFieldNames { + result = append(result, tealValueToFieldDesc(name, globals[fieldIdx])) + } + return result +} + +func prepareTxn(txn *transactions.Transaction, groupIndex int) []fieldDesc { + result := make([]fieldDesc, 0, len(logic.TxnFieldNames)) + for field, name := range logic.TxnFieldNames { + if field == int(logic.FirstValidTime) || + field == int(logic.Accounts) || + field == int(logic.ApplicationArgs) { + continue + } + var value string + var valType string = "string" + tv, err := logic.TxnFieldToTealValue(txn, groupIndex, logic.TxnField(field)) + if err != nil { + value = err.Error() + valType = "undefined" + } else { + value = tv.String() + valType = tealTypeMap[tv.Type] + } + result = append(result, fieldDesc{name, value, valType}) + } + return result +} + +func tealValueToFieldDesc(name string, tv basics.TealValue) fieldDesc { + var value string + var valType string + if tv.Type == basics.TealBytesType { + valType = "string" + data, err := base64.StdEncoding.DecodeString(tv.Bytes) + if err != nil { + value = tv.Bytes + } else { + printable := IsText(data) + if printable { + value = string(data) + } else if len(data) < 8 { + value = fmt.Sprintf("%q", data) + if value[0] == '"' { + value = value[1 : len(value)-1] + } + } else { + value = hex.EncodeToString(data) + } + } + } else { + valType = "number" + value = strconv.Itoa(int(tv.Uint)) + } + return fieldDesc{name, value, valType} +} + +func prepareArray(array []basics.TealValue) []fieldDesc { + result := make([]fieldDesc, 0, len(logic.TxnFieldNames)) + for i := 0; i < len(array); i++ { + tv := array[i] + name := strconv.Itoa(i) + result = append(result, tealValueToFieldDesc(name, tv)) + } + return result +} + +func makePreview(fields []fieldDesc) (prop []RuntimePropertyPreview) { + for _, field := range fields { + v := RuntimePropertyPreview{ + Name: field.Name, + Value: field.Value, + Type: field.Type, + } + prop = append(prop, v) + } + return +} + +func makeIntPreview(n int) (prop []RuntimePropertyPreview) { + for i := 0; i < n; i++ { + v := RuntimePropertyPreview{ + Name: strconv.Itoa(i), + Value: "Object", + Type: "object", + } + prop = append(prop, v) + } + return +} + +func makeTxnPreview(txnGroup []transactions.SignedTxn, groupIndex int) RuntimeObjectPreview { + var prop []RuntimePropertyPreview + if len(txnGroup) > 0 { + fields := prepareTxn(&txnGroup[groupIndex].Txn, groupIndex) + prop = makePreview(fields) + } + + p := RuntimeObjectPreview{Type: "object", Overflow: true, Properties: prop} + return p +} + +func makeGtxnPreview(txnGroup []transactions.SignedTxn) RuntimeObjectPreview { + prop := makeIntPreview(len(txnGroup)) + p := RuntimeObjectPreview{ + Type: "object", + Subtype: "array", + Description: fmt.Sprintf("Array(%d)", len(txnGroup)), + Overflow: false, + Properties: prop} + return p +} + +const maxArrayPreviewLength = 20 + +func makeArrayPreview(array []basics.TealValue) RuntimeObjectPreview { + fields := prepareArray(array) + + length := len(fields) + if length > maxArrayPreviewLength { + length = maxArrayPreviewLength + } + prop := makePreview(fields[:length]) + + p := RuntimeObjectPreview{ + Type: "object", + Subtype: "array", + Description: fmt.Sprintf("Array(%d)", len(array)), + Overflow: true, + Properties: prop} + return p +} + +func makeGlobalsPreview(globals []basics.TealValue) RuntimeObjectPreview { + fields := prepareGlobals(globals) + prop := makePreview(fields) + + p := RuntimeObjectPreview{ + Type: "object", + Description: "Object", + Overflow: true, + Properties: prop} + return p +} + +var gtxnObjIDPrefix = fmt.Sprintf("%s_gid_", gtxnObjID) + +func encodeGroupTxnID(groupIndex int) string { + return gtxnObjIDPrefix + strconv.Itoa(groupIndex) +} + +func decodeGroupTxnID(objID string) (int, bool) { + if strings.HasPrefix(objID, gtxnObjIDPrefix) { + if val, err := strconv.ParseInt(objID[len(gtxnObjIDPrefix):], 10, 32); err == nil { + return int(val), true + } + } + return 0, false +} + +func encodeArrayLength(objID string) string { + return fmt.Sprintf("%s_length", objID) +} + +func decodeArrayLength(objID string) (string, bool) { + if strings.HasSuffix(objID, "_length") { + if strings.HasPrefix(objID, stackObjID) { + return stackObjID, true + } else if strings.HasPrefix(objID, scratchObjID) { + return scratchObjID, true + } + } + return "", false +} + +func encodeArraySlice(objID string, fromIndex int, toIndex int) string { + return fmt.Sprintf("%s_%d_%d", objID, fromIndex, toIndex) +} + +func decodeArraySlice(objID string) (string, int, int, bool) { + if strings.HasPrefix(objID, stackObjID) || strings.HasPrefix(objID, scratchObjID) { + parts := strings.Split(objID, "_") + if len(parts) != 3 { + return "", 0, 0, false + } + var err error + var fromIndex, toIndex int64 + if fromIndex, err = strconv.ParseInt(parts[1], 10, 32); err != nil { + return "", 0, 0, false + } + if toIndex, err = strconv.ParseInt(parts[2], 10, 32); err != nil { + return "", 0, 0, false + } + return parts[0], int(fromIndex), int(toIndex), true + } + return "", 0, 0, false +} + +var appGlobalObjIDPrefix = fmt.Sprintf("%s_", appGlobalObjID) + +func encodeAppGlobalAppID(key string) string { + return appGlobalObjIDPrefix + key +} + +func decodeAppGlobalAppID(objID string) (uint64, bool) { + if strings.HasPrefix(objID, appGlobalObjIDPrefix) { + if val, err := strconv.ParseInt(objID[len(appGlobalObjIDPrefix):], 10, 32); err == nil { + return uint64(val), true + } + } + return 0, false +} + +var appLocalsObjIDPrefix = fmt.Sprintf("%s_", appLocalsObjID) + +func encodeAppLocalsAddr(addr string) string { + return appLocalsObjIDPrefix + addr +} + +func decodeAppLocalsAddr(objID string) (string, bool) { + if strings.HasPrefix(objID, appLocalsObjIDPrefix) { + return objID[len(appLocalsObjIDPrefix):], true + } + return "", false +} + +var appLocalAppIDPrefix = fmt.Sprintf("%s__", appLocalsObjID) + +func encodeAppLocalsAppID(addr string, appID string) string { + return fmt.Sprintf("%s%s_%s", appLocalAppIDPrefix, addr, appID) +} + +func decodeAppLocalsAppID(objID string) (string, uint64, bool) { + if strings.HasPrefix(objID, appLocalAppIDPrefix) { + encoded := objID[len(appLocalAppIDPrefix):] + parts := strings.Split(encoded, "_") + if val, err := strconv.ParseInt(parts[1], 10, 32); err == nil { + return parts[0], uint64(val), true + } + } + return "", 0, false +} + +func makeGlobalScope(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + globals := makeObject("globals", globalsObjID) + if preview { + globalsPreview := makeGlobalsPreview(s.globals) + globals.Value.Preview = &globalsPreview + } + + desc = []RuntimePropertyDescriptor{ + globals, + } + return desc +} + +func makeLocalScope(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + txn := makeObject("txn", txnObjID) + gtxn := makeArray("gtxn", len(s.txnGroup), gtxnObjID) + stack := makeArray("stack", len(s.stack), stackObjID) + scratch := makeArray("scratch", len(s.scratch), scratchObjID) + if preview { + txnPreview := makeTxnPreview(s.txnGroup, s.groupIndex) + if len(txnPreview.Properties) > 0 { + txn.Value.Preview = &txnPreview + } + gtxnPreview := makeGtxnPreview(s.txnGroup) + if len(gtxnPreview.Properties) > 0 { + gtxn.Value.Preview = >xnPreview + } + stackPreview := makeArrayPreview(s.stack) + if len(stackPreview.Properties) > 0 { + stack.Value.Preview = &stackPreview + } + scratchPreview := makeArrayPreview(s.scratch) + if len(scratchPreview.Properties) > 0 { + scratch.Value.Preview = &scratchPreview + } + } + + pc := makePrimitive(fieldDesc{ + Name: "PC", + Value: strconv.Itoa(s.pc.Load()), + Type: "number", + }) + desc = []RuntimePropertyDescriptor{ + pc, + txn, + gtxn, + stack, + scratch, + } + + if !s.appState.empty() { + var global, local RuntimePropertyDescriptor + if len(s.appState.global) > 0 { + global = makeObject("appGlobal", appGlobalObjID) + desc = append(desc, global) + } + if len(s.appState.locals) > 0 { + local = makeObject("appLocals", appLocalsObjID) + desc = append(desc, local) + } + } + + return desc +} + +func makeGlobals(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + fields := prepareGlobals(s.globals) + for _, field := range fields { + desc = append(desc, makePrimitive(field)) + } + return +} + +func makeTxn(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + if len(s.txnGroup) > 0 && s.groupIndex < len(s.txnGroup) && s.groupIndex >= 0 { + return makeTxnImpl(&s.txnGroup[s.groupIndex].Txn, s.groupIndex, preview) + } + return +} + +func makeTxnImpl(txn *transactions.Transaction, groupIndex int, preview bool) (desc []RuntimePropertyDescriptor) { + fields := prepareTxn(txn, groupIndex) + for _, field := range fields { + desc = append(desc, makePrimitive(field)) + } + return +} + +func makeTxnGroup(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + if len(s.txnGroup) > 0 { + for i := 0; i < len(s.txnGroup); i++ { + item := makeObject(strconv.Itoa(i), encodeGroupTxnID(i)) + if preview { + txnPreview := makeTxnPreview(s.txnGroup, i) + item.Value.Preview = &txnPreview + } + desc = append(desc, item) + } + } + return +} + +func makeAppGlobalState(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + for key := range s.appState.global { + s := strconv.Itoa(int(key)) + item := makeObject(s, encodeAppGlobalAppID(s)) + desc = append(desc, item) + } + return +} + +func makeAppLocalsState(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + for addr := range s.appState.locals { + a := addr.String() + item := makeObject(a, encodeAppLocalsAddr(a)) + desc = append(desc, item) + } + return +} + +func makeAppLocalState(s *cdtState, addr string) (desc []RuntimePropertyDescriptor) { + a, err := basics.UnmarshalChecksumAddress(addr) + if err != nil { + return + } + + if state, ok := s.appState.locals[a]; ok { + for key := range state { + s := strconv.Itoa(int(key)) + item := makeObject(s, encodeAppLocalsAppID(addr, s)) + desc = append(desc, item) + } + } + return +} + +func makeAppGlobalKV(s *cdtState, appID uint64) (desc []RuntimePropertyDescriptor) { + if tkv, ok := s.appState.global[basics.AppIndex(appID)]; ok { + return tkvToRpd(tkv) + } + return +} + +func makeAppLocalsKV(s *cdtState, addr string, appID uint64) (desc []RuntimePropertyDescriptor) { + a, err := basics.UnmarshalChecksumAddress(addr) + if err != nil { + return + } + + state, ok := s.appState.locals[a] + if !ok { + return + } + + if tkv, ok := state[basics.AppIndex(appID)]; ok { + return tkvToRpd(tkv) + } + return +} + +func tkvToRpd(tkv basics.TealKeyValue) (desc []RuntimePropertyDescriptor) { + for key, value := range tkv { + field := tealValueToFieldDesc(key, basics.TealValue{Type: value.Type, Uint: value.Uint, Bytes: value.Bytes}) + desc = append(desc, makePrimitive(field)) + } + return +} + +func makeArrayLength(array []basics.TealValue) (desc []RuntimePropertyDescriptor) { + field := fieldDesc{Name: "length", Value: strconv.Itoa(len(array)), Type: "number"} + desc = append(desc, makePrimitive(field)) + return +} + +func makeStackSlice(s *cdtState, from int, to int, preview bool) (desc []RuntimePropertyDescriptor) { + // temporary disable stack reversion to see if people prefer appending to the list + // stack := make([]v2.TealValue, len(s.stack)) + // for i := 0; i < len(stack); i++ { + // stack[i] = s.stack[len(s.stack)-1-i] + // } + + stack := s.stack[from : to+1] + fields := prepareArray(stack) + for _, field := range fields { + desc = append(desc, makePrimitive(field)) + } + field := fieldDesc{Name: "length", Value: strconv.Itoa(len(s.stack)), Type: "number"} + desc = append(desc, makePrimitive(field)) + return +} + +func makeStack(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + return makeStackSlice(s, 0, len(s.stack)-1, preview) +} + +func makeScratchSlice(s *cdtState, from int, to int, preview bool) (desc []RuntimePropertyDescriptor) { + scratch := s.scratch[from : to+1] + fields := prepareArray(scratch) + for _, field := range fields { + desc = append(desc, makePrimitive(field)) + } + field := fieldDesc{Name: "length", Value: strconv.Itoa(len(scratch)), Type: "number"} + desc = append(desc, makePrimitive(field)) + return +} + +func makeScratch(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + return makeScratchSlice(s, 0, len(s.scratch)-1, preview) +} + +func makeTealError(s *cdtState, preview bool) (desc []RuntimePropertyDescriptor) { + if lastError := s.err.Load(); len(lastError) != 0 { + field := fieldDesc{Name: "message", Value: lastError, Type: "string"} + desc = append(desc, makePrimitive(field)) + } + return +} diff --git a/cmd/tealdbg/cdtdbg.go b/cmd/tealdbg/cdtdbg.go new file mode 100644 index 0000000000..2f7f720e38 --- /dev/null +++ b/cmd/tealdbg/cdtdbg.go @@ -0,0 +1,181 @@ +// 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 . + +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/algorand/go-deadlock" + "github.com/gorilla/mux" +) + +// CDTAdapter is Chrome DevTools frontend +type CDTAdapter struct { + mu deadlock.Mutex + sessions map[string]cdtSession + router *mux.Router + apiAddress string + verbose bool +} + +// CDTAdapterParams for Setup +type CDTAdapterParams struct { + router *mux.Router + apiAddress string + verbose bool +} + +// MakeCDTAdapter creates new CDTAdapter +func MakeCDTAdapter(ctx interface{}) (a *CDTAdapter) { + params, ok := ctx.(*CDTAdapterParams) + if !ok { + panic("MakeCDTAdapter expected CDTAdapterParams") + } + + a = new(CDTAdapter) + + a.sessions = make(map[string]cdtSession) + a.router = params.router + a.apiAddress = params.apiAddress + a.verbose = params.verbose + + a.router.HandleFunc("/json/version", a.versionHandler).Methods("GET") + a.router.HandleFunc("/json", a.jsonHandler).Methods("GET") + a.router.HandleFunc("/json/list", a.jsonHandler).Methods("GET") + + return a +} + +// SessionStarted registers new session +func (a *CDTAdapter) SessionStarted(sid string, debugger Control, ch chan Notification) { + s := makeCDTSession(sid, debugger, ch) + + a.mu.Lock() + defer a.mu.Unlock() + + s.endpoint = a.enableWebsocketEndpoint(sid, a.apiAddress, s.websocketHandler) + + if name, source := debugger.GetSource(); len(source) != 0 { + s.scriptURL = name + s.sourceMapURL = fmt.Sprintf("http://%s/%s/sourcemap", a.apiAddress, sid) + a.router.HandleFunc(fmt.Sprintf("/%s/sourcemap", sid), s.sourceMapHandler).Methods("GET") + a.router.HandleFunc(fmt.Sprintf("/%s/source", sid), s.sourceHandler).Methods("GET") + } + + s.verbose = a.verbose + s.states = debugger.GetStates(nil) + + a.sessions[sid] = *s +} + +// SessionEnded removes the session +func (a *CDTAdapter) SessionEnded(sid string) { + go func() { + a.mu.Lock() + s := a.sessions[sid] + a.mu.Unlock() + + <-s.done + + a.mu.Lock() + delete(a.sessions, sid) + a.mu.Unlock() + log.Printf("CDT session %s closed\n", sid) + }() +} + +// WaitForCompletion returns when no active connections left +func (a *CDTAdapter) WaitForCompletion() { + for { + a.mu.Lock() + active := len(a.sessions) + a.mu.Unlock() + if active == 0 { + return + } + time.Sleep(100 * time.Millisecond) + } +} + +// must be called with rctx.mux locked +func (a *CDTAdapter) enableWebsocketEndpoint( + uuid string, apiAddress string, + handler func(http.ResponseWriter, *http.Request), +) cdtTabDescription { + address := apiAddress + "/" + uuid + desc := cdtTabDescription{ + Description: "", + ID: uuid, + Title: "Algorand TEAL program", + TabType: "node", + URL: "https://algorand.com/", + DevtoolsFrontendURL: "chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=false&ws=" + address, + DevtoolsFrontendURLCompat: "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=false&ws=" + address, + WebSocketDebuggerURL: "ws://" + address, + FaviconURL: "https://www.algorand.com/icons/icon-144x144.png", + } + + a.router.HandleFunc("/"+uuid, handler) + + log.Println("------------------------------------------------") + log.Printf("CDT debugger listening on: %s", desc.WebSocketDebuggerURL) + log.Printf("Or open in Chrome:") + log.Printf("%s", desc.DevtoolsFrontendURL) + log.Println("------------------------------------------------") + + return desc +} + +func (a *CDTAdapter) versionHandler(w http.ResponseWriter, r *http.Request) { + type devtoolsVersion struct { + Browser string `json:"Browser"` + ProtocolVersion string `json:"Protocol-Version"` + } + + version := devtoolsVersion{Browser: "Algorand TEAL Debugger", ProtocolVersion: "1.1"} + enc, err := json.Marshal(version) + if err != nil { + return + } + w.WriteHeader(http.StatusOK) + w.Write(enc) + return +} + +func (a *CDTAdapter) jsonHandler(w http.ResponseWriter, r *http.Request) { + tabs := make([]cdtTabDescription, 0, len(a.sessions)) + + func() { + a.mu.Lock() + defer a.mu.Unlock() + for _, s := range a.sessions { + tabs = append(tabs, s.endpoint) + } + }() + + enc, err := json.Marshal(tabs) + if err != nil { + return + } + w.WriteHeader(http.StatusOK) + w.Write(enc) + return +} diff --git a/cmd/tealdbg/debugger.go b/cmd/tealdbg/debugger.go new file mode 100644 index 0000000000..3b5d720697 --- /dev/null +++ b/cmd/tealdbg/debugger.go @@ -0,0 +1,489 @@ +// 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 . + +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/algorand/go-deadlock" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions/logic" +) + +// Notification is sent to the client over their websocket connection +// on each new TEAL execution/update/complation +type Notification struct { + Event string `codec:"event"` + DebugState logic.DebugState `codec:"state"` +} + +// DebugAdapter represents debugger frontend (i.e. CDT, webpage, VSCode, etc) +type DebugAdapter interface { + SessionStarted(sid string, debugger Control, ch chan Notification) + SessionEnded(sid string) + WaitForCompletion() +} + +// Control interface for execution control +type Control interface { + Step() + Resume() + SetBreakpoint(line int) error + RemoveBreakpoint(line int) error + SetBreakpointsActive(active bool) + + GetSourceMap() ([]byte, error) + GetSource() (string, []byte) + GetStates(changes *logic.AppStateChage) appState +} + +// Debugger is TEAL event-driven debugger +type Debugger struct { + mus deadlock.Mutex + sessions map[string]*session + programs map[string]*programMeta + + mud deadlock.Mutex + das []DebugAdapter +} + +// MakeDebugger creates Debugger instance +func MakeDebugger() *Debugger { + d := new(Debugger) + d.sessions = make(map[string]*session) + d.programs = make(map[string]*programMeta) + return d +} + +type programMeta struct { + name string + program []byte + source string + offsetToLine map[int]int + states appState +} + +type debugConfig struct { + // If -1, don't break + BreakAtLine int `json:"breakatline"` +} + +type session struct { + mu deadlock.Mutex + // Reply to registration/update when bool received on acknowledgement + // channel, allowing program execution to continue + acknowledged chan bool + + // debugConfigs holds information about this debugging session, + // currently just when we want to break + debugConfig debugConfig + + // notifications from eval + notifications chan Notification + + // program that is being debugged + disassembly string + lines []string + + programName string + program []byte + source string + offsetToLine map[int]int // pc to source line + pcOffset map[int]int // disassembly line to pc + + breakpoints []breakpoint + line atomicInt + + states appState +} + +type breakpoint struct { + set bool + active bool +} + +func (bs *breakpoint) NonEmpty() bool { + return bs.set +} + +func makeSession(disassembly string, line int) (s *session) { + s = new(session) + + // Allocate a default debugConfig (don't break) + s.debugConfig = debugConfig{ + BreakAtLine: -1, + } + + // Allocate an acknowledgement and notifications channels + s.acknowledged = make(chan bool) + s.notifications = make(chan Notification) + + s.disassembly = disassembly + s.lines = strings.Split(disassembly, "\n") + s.breakpoints = make([]breakpoint, len(s.lines)) + s.line.Store(line) + return +} + +func (s *session) resume() { + select { + case s.acknowledged <- true: + default: + } +} + +func (s *session) Step() { + func() { + s.mu.Lock() + defer s.mu.Unlock() + s.debugConfig = debugConfig{BreakAtLine: 0} + }() + + s.resume() +} + +func (s *session) Resume() { + currentLine := s.line.Load() + + func() { + s.mu.Lock() + defer s.mu.Unlock() + s.debugConfig = debugConfig{BreakAtLine: -1} // reset possible break after Step + // find any active breakpoints and set next break + if currentLine < len(s.breakpoints) { + for line, state := range s.breakpoints[currentLine+1:] { + if state.set && state.active { + s.setBreakpoint(line + currentLine + 1) + break + } + } + } + }() + + s.resume() +} + +// setBreakpoint must be called with lock taken +func (s *session) setBreakpoint(line int) error { + if line >= len(s.breakpoints) { + return fmt.Errorf("invalid bp line %d", line) + } + s.breakpoints[line] = breakpoint{set: true, active: true} + s.debugConfig = debugConfig{BreakAtLine: line} + return nil +} + +func (s *session) SetBreakpoint(line int) error { + s.mu.Lock() + defer s.mu.Unlock() + return s.setBreakpoint(line) +} + +func (s *session) RemoveBreakpoint(line int) error { + s.mu.Lock() + defer s.mu.Unlock() + + if line < 0 || line >= len(s.breakpoints) { + return fmt.Errorf("invalid bp line %d", line) + } + if s.breakpoints[line].NonEmpty() { + s.debugConfig = debugConfig{BreakAtLine: -1} + s.breakpoints[line] = breakpoint{} + } + return nil +} + +func (s *session) SetBreakpointsActive(active bool) { + s.mu.Lock() + defer s.mu.Unlock() + + for i := 0; i < len(s.breakpoints); i++ { + if s.breakpoints[i].NonEmpty() { + s.breakpoints[i].active = active + } + } + if !active { + s.debugConfig = debugConfig{BreakAtLine: -1} + } +} + +// GetSourceMap creates source map from source, disassembly and mappings +func (s *session) GetSourceMap() ([]byte, error) { + if len(s.source) == 0 { + return nil, nil + } + + type sourceMap struct { + Version int `json:"version"` + File string `json:"file"` + SourceRoot string `json:"sourceRoot"` + Sources []string `json:"sources"` + Mappings string `json:"mappings"` + } + lines := make([]string, len(s.lines)) + const targetCol int = 0 + const sourceIdx int = 0 + sourceLine := 0 + const sourceCol int = 0 + prevSourceLine := 0 + + // the very first entry is needed by CDT + lines[0] = MakeSourceMapLine(targetCol, sourceIdx, 0, sourceCol) + for targetLine := 1; targetLine < len(s.lines); targetLine++ { + if pc, ok := s.pcOffset[targetLine]; ok && pc != 0 { + sourceLine, ok = s.offsetToLine[pc] + if !ok { + lines[targetLine] = "" + } else { + lines[targetLine] = MakeSourceMapLine(targetCol, sourceIdx, sourceLine-prevSourceLine, sourceCol) + prevSourceLine = sourceLine + } + } else { + delta := 0 + // the very last empty line, increment by number src number by 1 + if targetLine == len(s.lines)-1 { + delta = 1 + } + lines[targetLine] = MakeSourceMapLine(targetCol, sourceIdx, delta, sourceCol) + } + } + + sm := sourceMap{ + Version: 3, + File: s.programName + ".dis", + SourceRoot: "", + Sources: []string{"source"}, // this is a pseudo source file name, served by debugger + Mappings: strings.Join(lines, ";"), + } + data, err := json.Marshal(&sm) + return data, err +} + +func (s *session) GetSource() (string, []byte) { + if len(s.source) == 0 { + return "", nil + } + return s.programName, []byte(s.source) +} + +func (s *session) GetStates(changes *logic.AppStateChage) appState { + if changes == nil { + return s.states + } + + newStates := s.states.clone() + appIdx := newStates.appIdx + + applyDelta := func(sd basics.StateDelta, tkv basics.TealKeyValue) { + for key, delta := range sd { + switch delta.Action { + case basics.SetUintAction: + tkv[key] = basics.TealValue{Type: basics.TealUintType, Uint: delta.Uint} + case basics.SetBytesAction: + tkv[key] = basics.TealValue{ + Type: basics.TealBytesType, Bytes: delta.Bytes, + } + case basics.DeleteAction: + delete(tkv, key) + } + } + } + + if len(changes.GlobalStateChanges) > 0 { + tkv := newStates.global[appIdx] + if tkv == nil { + tkv = make(basics.TealKeyValue) + } + applyDelta(changes.GlobalStateChanges, tkv) + newStates.global[appIdx] = tkv + } + + for addr, delta := range changes.LocalStateChanges { + local := newStates.locals[addr] + if local == nil { + local = make(map[basics.AppIndex]basics.TealKeyValue) + } + tkv := local[appIdx] + if tkv == nil { + tkv = make(basics.TealKeyValue) + } + applyDelta(delta, tkv) + local[appIdx] = tkv + newStates.locals[addr] = local + } + + return newStates +} + +func (d *Debugger) getSession(sid string) (s *session, err error) { + d.mus.Lock() + defer d.mus.Unlock() + s, ok := d.sessions[sid] + if !ok { + err = fmt.Errorf("session %s not found", sid) + } + return +} + +func (d *Debugger) createSession(sid string, disassembly string, line int, pcOffset map[int]int) (s *session) { + d.mus.Lock() + defer d.mus.Unlock() + + s = makeSession(disassembly, line) + d.sessions[sid] = s + meta, ok := d.programs[sid] + if ok { + s.programName = meta.name + s.program = meta.program + s.source = meta.source + s.offsetToLine = meta.offsetToLine + s.pcOffset = pcOffset + s.states = meta.states + } + return +} + +func (d *Debugger) removeSession(sid string) (s *session) { + d.mus.Lock() + defer d.mus.Unlock() + + delete(d.sessions, sid) + return +} + +// AddAdapter adds a new debugger adapter +func (d *Debugger) AddAdapter(da DebugAdapter) { + d.mud.Lock() + defer d.mud.Unlock() + d.das = append(d.das, da) +} + +// hashProgram returns binary program hash +func (d *Debugger) hashProgram(program []byte) string { + hash := sha256.Sum256([]byte(program)) + return hex.EncodeToString(hash[:]) +} + +// SaveProgram stores program, source and offsetToLine for later use +func (d *Debugger) SaveProgram( + name string, program []byte, source string, offsetToLine map[int]int, + states appState, +) { + hash := d.hashProgram(program) + d.mus.Lock() + defer d.mus.Unlock() + d.programs[hash] = &programMeta{ + name, + program, + source, + offsetToLine, + states, + } +} + +// Register setups new session and notifies frontends if any +func (d *Debugger) Register(state *logic.DebugState) error { + sid := state.ExecID + pcOffset := make(map[int]int, len(state.PCOffset)) + for _, pco := range state.PCOffset { + pcOffset[state.PCToLine(pco.PC)] = pco.PC + } + s := d.createSession(sid, state.Disassembly, state.Line, pcOffset) + + // Store the state for this execution + d.mud.Lock() + for _, da := range d.das { + da.SessionStarted(sid, s, s.notifications) + } + d.mud.Unlock() + + // TODO: Race or deadlock possible here: + // 1. registered sent, context switched + // 2. Non-blocking Resume() called in onRegistered handler on not ready acknowledged channel + // 3. context switched back and blocked on <-s.acknowledged below + // + // How to fix: + // make Resume() synchronous but special handling needed for already completed programs + + // Inform the user to configure execution + s.notifications <- Notification{"registered", *state} + + // Wait for acknowledgement + <-s.acknowledged + + return nil +} + +// Update process state update notifications: pauses or continues as needed +func (d *Debugger) Update(state *logic.DebugState) error { + sid := state.ExecID + s, err := d.getSession(sid) + if err != nil { + return err + } + s.line.Store(state.Line) + + go func() { + // Check if we are triggered and acknowledge asynchronously + cfg := s.debugConfig + if cfg.BreakAtLine != -1 { + if cfg.BreakAtLine == 0 || state.Line == cfg.BreakAtLine { + // Breakpoint hit! Inform the user + s.notifications <- Notification{"updated", *state} + } else { + // Continue if we haven't hit the next breakpoint + s.acknowledged <- true + } + } else { + // User won't send acknowledgment, so we will + s.acknowledged <- true + } + }() + + // Let TEAL continue when acknowledged + <-s.acknowledged + + return nil +} + +// Complete terminates session and notifies frontends if any +func (d *Debugger) Complete(state *logic.DebugState) error { + sid := state.ExecID + s, err := d.getSession(sid) + if err != nil { + return err + } + + // Inform the user + s.notifications <- Notification{"completed", *state} + + // Clean up exec-specific state + d.removeSession(sid) + + d.mud.Lock() + for _, da := range d.das { + da.SessionEnded(sid) + } + d.mud.Unlock() + + return nil +} diff --git a/cmd/tealdbg/debugger_test.go b/cmd/tealdbg/debugger_test.go new file mode 100644 index 0000000000..2bbb80e9a8 --- /dev/null +++ b/cmd/tealdbg/debugger_test.go @@ -0,0 +1,119 @@ +// 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 . + +package main + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/protocol" +) + +type testDbgAdapter struct { + debugger Control + notifications chan Notification + + started bool + ended bool + eventCount int + + done chan struct{} + + t *testing.T +} + +func makeTestDbgAdapter(t *testing.T) (d *testDbgAdapter) { + d = new(testDbgAdapter) + d.done = make(chan struct{}) + d.t = t + return d +} + +func (d *testDbgAdapter) WaitForCompletion() { + <-d.done +} + +func (d *testDbgAdapter) SessionStarted(sid string, debugger Control, ch chan Notification) { + d.debugger = debugger + d.notifications = ch + + go d.eventLoop() + + d.started = true +} + +func (d *testDbgAdapter) SessionEnded(sid string) { + d.ended = true +} + +func (d *testDbgAdapter) eventLoop() { + for { + select { + case n := <-d.notifications: + d.eventCount++ + if n.Event == "completed" { + d.done <- struct{}{} + return + } + if n.Event == "registered" { + require.NotNil(d.t, n.DebugState.Globals) + require.NotNil(d.t, n.DebugState.Scratch) + require.NotEmpty(d.t, n.DebugState.Disassembly) + require.NotEmpty(d.t, n.DebugState.ExecID) + d.debugger.SetBreakpoint(n.DebugState.Line + 1) + } + // simulate user delay to workaround race cond + time.Sleep(10 * time.Millisecond) + d.debugger.Resume() + } + } +} + +func TestDebuggerSimple(t *testing.T) { + proto := config.Consensus[protocol.ConsensusV23] + debugger := MakeDebugger() + + da := makeTestDbgAdapter(t) + debugger.AddAdapter(da) + + ep := logic.EvalParams{ + Proto: &proto, + Debugger: debugger, + Txn: &transactions.SignedTxn{}, + } + + source := `int 0 +int 1 ++ +` + program, err := logic.AssembleStringV1(source) + require.NoError(t, err) + + _, err = logic.Eval(program, ep) + require.NoError(t, err) + + da.WaitForCompletion() + + require.True(t, da.started) + require.True(t, da.ended) + require.Equal(t, 3, da.eventCount) // register, update, complete +} diff --git a/cmd/tealdbg/dryrunRequest.go b/cmd/tealdbg/dryrunRequest.go new file mode 100644 index 0000000000..482a47afe5 --- /dev/null +++ b/cmd/tealdbg/dryrunRequest.go @@ -0,0 +1,104 @@ +// 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 . + +package main + +import ( + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" + + v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" + generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" +) + +// ddrFromParams converts serialized DryrunRequest to v2.DryrunRequest +func ddrFromParams(dp *DebugParams) (ddr v2.DryrunRequest, err error) { + if len(dp.DdrBlob) == 0 { + return + } + + var gdr generatedV2.DryrunRequest + err = protocol.DecodeJSON(dp.DdrBlob, &gdr) + if err == nil { + ddr, err = v2.DryrunRequestFromGenerated(&gdr) + } else { + err = protocol.DecodeReflect(dp.DdrBlob, &ddr) + } + + return +} + +func convertAccounts(accounts []generatedV2.Account) (records []basics.BalanceRecord, err error) { + for _, a := range accounts { + var addr basics.Address + addr, err = basics.UnmarshalChecksumAddress(a.Address) + if err != nil { + return + } + var ad basics.AccountData + ad, err = v2.AccountToAccountData(&a) + if err != nil { + return + } + records = append(records, basics.BalanceRecord{Addr: addr, AccountData: ad}) + } + return +} + +func balanceRecordsFromDdr(ddr *v2.DryrunRequest) (records []basics.BalanceRecord, err error) { + accounts := make(map[basics.Address]basics.AccountData) + for _, a := range ddr.Accounts { + var addr basics.Address + addr, err = basics.UnmarshalChecksumAddress(a.Address) + if err != nil { + return + } + var ad basics.AccountData + ad, err = v2.AccountToAccountData(&a) + if err != nil { + return + } + accounts[addr] = ad + } + for _, a := range ddr.Apps { + var addr basics.Address + addr, err = basics.UnmarshalChecksumAddress(a.Params.Creator) + if err != nil { + return + } + appIdx := basics.AppIndex(a.Id) + var ad basics.AccountData + var ok bool + if ad, ok = accounts[addr]; ok { + // skip if this app params are already set + if _, ok = ad.AppParams[appIdx]; ok { + continue + } + } + // deserialize app params and update account data + params := v2.ApplicationParamsToAppParams(&a.Params) + if ad.AppParams == nil { + ad.AppParams = make(map[basics.AppIndex]basics.AppParams, 1) + } + ad.AppParams[appIdx] = params + accounts[addr] = ad + } + + for addr, ad := range accounts { + records = append(records, basics.BalanceRecord{Addr: addr, AccountData: ad}) + } + return +} diff --git a/cmd/tealdbg/home.html b/cmd/tealdbg/home.html new file mode 100644 index 0000000000..c900192d27 --- /dev/null +++ b/cmd/tealdbg/home.html @@ -0,0 +1,336 @@ + + + + + + +

waiting for connection from TEAL...

+ + +
+
+ + + + + + diff --git a/cmd/tealdbg/homepage.go b/cmd/tealdbg/homepage.go new file mode 100644 index 0000000000..fadef9afc5 --- /dev/null +++ b/cmd/tealdbg/homepage.go @@ -0,0 +1,357 @@ +// 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 . + +// Code generated during build process. DO NOT EDIT. +package main + +var homepage string = ` + + + + + + +

waiting for connection from TEAL...

+ + +
+
+ + + + + + +` diff --git a/cmd/tealdbg/images/cdt-controls.png b/cmd/tealdbg/images/cdt-controls.png new file mode 100644 index 0000000000000000000000000000000000000000..47109cc0f40551559d7818c36abbc88323d986f8 GIT binary patch literal 297786 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 c8(At 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 TXSTP40SG)@{an^LB{Ts5p9U2j literal 0 HcmV?d00001 diff --git a/cmd/tealdbg/images/cdt-screenshot.png b/cmd/tealdbg/images/cdt-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d0570703f8319ad88aadb099bc9074e9f4d32ed7 GIT binary patch literal 211793 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%0ALj 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

@K$XhONVYt%%-?%&YTG^aMk&Yygxi012 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

    jN@_po z^cE6EA<-EDIF`x$Eb+e@goqS^tHOe==<#O$ptq2*4SKXgJDSraq4A@$wL3R4c(VTa zzpf>MOX2rw`&Hg-Zjq5BwC44xrCFQU6ZR_7=ylXN&Oq>m3;<+bc|BJ-{sC%*rHR^o zV*IS}?yo%;ylr6@rXEc>9+pb|Op=oL01|ePa4gC7zwY7D5F~1Up%SR`bn42e-rYE< zZdp=sxNDIUQB(>j?iX~A0Q2U*SY_G4{}16E$zP8qzS1t@vTEltF!!@WUL=~+DaJoR zmmxaxgM+u~KpVj4Zm*j4r~iY{@++gxyFVvIH#=&nd;g%*(2QPmjrUh8Y7<7lhA%-< z9nyjN>L1sVj_oP+4|%L0Mno*mLR%4DaI#!}gYM4h%@74I=g5G@Anr~Zh9FMhnc0f} z4;wsS1A)Vl4Lg-F{Y>SseynC}7F=Kby3iHfY_7vNqfU+u$StE02|u>{m&fUZFp;|0 zmHtDj%XTH+(>DHs^HN{+X;r&}*UFH7ag{Xvn`RlCht?(TpI*$40y<^=lR@C5>k%0Q z?Ct!gKEa}2)lGf?#H)Q4IWO{0k-nQkFp2y`%+@r?C#^dr?Gu%YPDIaRuFbIgMz_B$q=zT z@o!SA#BFY^=3JnCzFyXdL=ZpmvN>Y?=SzjLK;JxMJ{`zynp@}H4HHUg|Mozu>lp%I zX>#?6*U)*P|0=BqguOGaq_kCOZeD)S^ju=%2Z`pk1k8hFu3Qh=R8M$2Xwq9=uABcd zgUt@&msT|RQKHGR{gO*YDhaxzM`E=nYz&~ z`o795h+Hwwz*sTH*Q)=oKDiI!Z+rydX|*USit;8_sPF8il+6|yVoY#F6m-h1CtuvYl z^6yJeB8BF1N!Z@lBCWSWQ)_~(;G+&QuFi~b!qn^e>G*v%rD zDAviUREH~k_T`UqN2mEDcFOi?-Xm#|erhcJpSG(ctmj9|LEig|Tl{LbV;_#ZKZAMG z)-4f3N~fh!de|pX`E@_VLEoOr4*zVk42A5b&n!xTL=NB;#p)mJ7Glwj7pQX_8TFu#WGop|8%n zB(+8TUK|D&fiD*$XKEWWsAm%&mx4S566&WL3Y^Yt)@<^37#7*tS#%uQe(Wl}oZ<2! zRS|7%T9iDQ%SxWE&>YYp9UkR-aZhP|J~sWK1lf?nKzrJP92S@9`VcD%POkTNQ5rkrsG&o0x2|Se+w4CY7FpSw&2R{v`Kwv=&G%%kZc-&UirFi-(=D0rBQPqc}kvjw{ENb8Lqvb$#QCQM*W<5l zsz;FU!X)7A+5K@Pl!HCGk$25ukgf_z`eZ0xsxGb_s^PZvuN_=hGr|6q?a&oXWOleC zzn!77|1ezpTB3;eXCWG|ft>PsNZ|pC_T=Y_y%L&I-_J3ypJPp(qkn(m4rhk2SNH-C zZ?d-*_n0TS0O2oSYJ>JpzN$MmtCf1P*+&7#-i)rO$Q0zL$opikrJm4_peaj5WZ=y1 zFFDrQGMn$Uz92HSR*TbNg%`uFduiEhI+sQDr+2b1b4y}>tvnF}4mD35hcj^3-XrAjtT3T+qox(uI5lo-b` zYkiAeJzOBW=u`TgE4y>V(#P_nooAqJ8962=U&p5*q>it_s@<3E%PG5I6?>aPRiP^H zmE&>w%gyM=B}Ex(;f2Zk6CyrfWxjv{L162V^uIicA^Y6LANwe}TE(>mcuG~7QIu#L z;1M=2KKzO~gWJCIjlt$sH~YPv{Wuu7X`IbmaZ$ZNmx@V&L10Cc7n0AVZbY^{*f0m> zJhya<7Z+G^Gn|hf3=LVYPbPwttp5<$oM?@#!uUaW2=64Dor;{$^J4cRYWv4)%`0?5eeGS91&BxXX3Zf$hRNlU-jy+)1;mcYcSj4Dp&4 zN{Wu$Y<-0-d$aL`FNAw%gY4lQXhVNACZ3K6!BMK>Lv{Cvb>c1j$VK=?U6mBXEu3jr z0Uo_>VB`F@o$`GuNyawe*l>LH6e}tL1)djmNV&QtoMEt$!_FMJo5xg#*Zu^r0wk2@5OlRAz>hnD?qs z_j_yrZ&H90J8{pKM;Pa}P^eTfnij`hef)sAjTH+CGLz(OvO6)py}G!Id)p=J1;!cy z9^Z@SwU%vJR!f8=rs5)flVQwv$_K<=~8-LA%<>R~pLnEfw8?%13W z+N69XSnmh;Y*VQjGaL}oO=3nnw8pA*;{-1jVt%KJ2y*swJnM#{euzR_kR~Z9g+` zWYG}kFSEnMe`q~wzWgXaN~?tI!>F-|`#A~%Th|2GRbZnJOkkn1ryqhl_Z$TBz)H#H z*HM#cjMj_ycV1UukZZ8><(o>^>Fax*CX%(^ICG{h!*;X-R||^mZcI4@5#Cft-SZQe zDsmNpA&5C2`8S-!G&Xp-lzP7#yexDFb|n716uvJS1|*21aoE`ZV_H%X#sq~bGJW%_EeItle5l}X{Ud{Kb=O0dSNq!Nc~k;T;^b4XdCga1x)JF*d=_{yGxfU^ zwQ%$>8!z{82W!W0M_e!8&EF)4PkLc5=VYX*g%%@RpJY{^yybCui?)XaRaYpR zDvs`}RgRts!gFGqegyZEWb;0h={aG0U2|QxACAnsKd7SEkg-dITE@RK`%5%T>ArKq z#L&Q*;?T*d36?O~j#rtGlf(5f=h0!lQH`#8j;r-!)BKA&e##M?hfaN20%;8Lk z7zr!Kc>yzFayHB~OzQX|pP9IOaA9bm z#mXkvxniTF6=|sM%K0lFoNrmbxcW@};SSws`OCIk`og8UO z(lm=8rhdjb-a_J+XqXRD2+oNP3FzVaS`w)r2UbWZU@11#CwIcDFmgck-87k7PIX90 z4zj3OFk3K0@A|8VNH6nOZceL|eQHqY63fi=&QdigD2mfz5Yf7&N_Ewp+o6Duw9!NrJ(~%-#E+<*2ZlN_eNMH0#IF?NxuKk)l)3B0zdB$+8V&~YYlGDf;X}O83a)}ulV<201e2_<7BOsz0jy?o^qy87F`qLe3RJhS!rgvUmX zqN_B-4i$0(JA@~bawZ@lqRhC=hpA1;K8vt_tx|dc^6_>ybiqFzSs!Kf63LpER(=ks zK>FlmkVi@af={|!5h6@8kFvA7*`UK37xMK@bY(6Ud3v{-#&&oK#IcCX;4xcozHrpv5prDp?ynEW zTdS45;h9wn2~Q?!_r3-aLE%yso z4?n#BN{h_B)V&Ws;{aJ!Fpxyo8D?oa zxQj(e3mT)Cxvp#m&qz`~UeB2nyDp?VEdh~93#LxERXPMLIKaUq29brt#ZNGdd=)x- zf^nmaXMAosIdZ!E#D11?EOrsfe4ti%zn&RauJb6HJR@>JzwEUkkpW)%DfyyB6MyoL zESp&4kMw(+b)25ARZMQjDZGIvWI>z>|655KLQbR%xn-690`C3O=A=_+uQOrxjXLT` zD7^xKyGE4O&Yn>79zk1o#|?CJv?g-<4KmaJnZw`b^w48FYgL6e>MBPrg0 z{k(z!v!c&rdoK}_22CT2yqCG{2G1_Q)NLVA2?6U%=Z387A2vqN%&|h-M?+|C7phZM z-lE3w|JZxWrns{2k2knW2pWRB6C}6>g1fszAhKgRK5= z%IA0XqCH)g{c?cFX>qbOF}It94t0AoYz*I~rdO_fX7al(1=CC1H-l*J2CX&cbSZvb zuAVhLME5DN+0Bb+V%^>CqvXwtT2L+<*Aj0zeAi)}GJ`Mll;^9UYD4rq^R%hx_smLW zL;j>C05r)d(Y0F65s%8ZX}&VD9)UyK&zp}^xioo&=nZX%P5#**zwM37MjoWamu$!9 zFXdK}3AU*HH*jDV9@eW0PG`;#6QNp?pHSRSxxnM98y~MDg(ow<+Dv7$ZoB%XSTZ`& zhrw1#C*!{dy?yNui(={bnkqs^Sq(P%JN0j~c82+%eZ{MXQ)e;O-*JfpV2SP6UO!w% zB|`>_KCukkhLy!XPm=Kpk(|J5=XiwxZz-tECH`EZ69BotA%fmFMwb?1Y)Jo)1&}<# zJ$#^AQwRg}zG_{T=7*CXho*bnwL|79*Wlq{xJp zyF)M+?>TTG^jwP5Vhv99HW!P!*4Ztse@I`HTnZ1H^Q=hAoFU-0Ke>d_g2U=^O}#S@ zF_vx8Mn%f`v6s^->0LPcCoqhI|wCNp7-&H|Q+T82lmxbuw zb|K9!l;#DcpJB{j-~J$S*?S%+X2F{U4TfS^I)rxYkbsgro+qjEwxG5|TE7b$XJCne zppk%4&~dSm`^(EwIl8!78_Sy^>InhfEiS(wvI8fv&WZ3eBA1b|FykcPGp{tRskc24 z;2b}uyz{h&e}k-%i8(bw&e6gPufnoI$?hbuxzNfii3Jx@pD7YDs$i z-UT&@q^+2?K=h_La z&P1k+ZKQIs^~oRK>gto=`QS??=0Y8|B(p8$DehMglA3X24zM3Huuf|~V*_&>7QHvt zHD=ABiS>Z7YYRZ;yeD)G_#r+j4s*RFLCqIN58XUH9QTuLld|R);$T4oAgG@`0_}qb?gbr zx5e=GpF}4t{!kWKdRRo{-j$^)NS^2`s`S^8&zLBj`+K5jQngTzdMp$v70ES?bj`;+khBIWN<19E|cdtJ8gNOQb>bded;5y+nNdW zQhsN8fM=F49;}1TI&eqW&tcdF?5)e*bSe;YTh!of5S<^}8@-os0!G&1yAj2WPeC88 zxZX;FgN)s%IpKR5;Axc%p`+wjdCuMlS?l2^(mabyB+AdW^NdeSk5gRN5ka&a&*_ns z`72UW8fsQ6Me)jRQyWTh!6nsL0WR?|F$k zOFf%sI@>ekZ%yNd?q6EmhO|qP?tzb2acyOD@uCqm_8~6@6wzhN#k1ZHr<}j0Z#)PF)PmmJpUe7I}#@B+OwEy(8=>%o)|kx%~n z5vI4iQddi9jFev)l29OhQyD+|(Ol=|2)R0vH4ZC`C%mL{&Dg(^wQMItu)uPBo}>3_ zlW<6zgXjEM87K)43XM@=iOHIu`RR}ZuprfQ#Wp0X{v$??{Hq5des|)Q)I;5Fnd~^j zb&0|YOS5b+3gqU@U>la0X0%3(v1{M@8MwF5lr7K?R)T~4Yx^_L9gz<;?Y3x>!98*K zC0r|WueL5o>i%5#x_a&1WoI)c_kJGq@He%9BQ{;>V(NxV!dH!HAg77tba>5&gE{Ea z-RttPaN-5EE`uu&s@n`2S1EQEFNfJ8qoa-N|FGnc)%qhS`FQZ#Wck|L0=*Q zlbG(c%*E?h-X6vsoX%&5i&@#(dy=BCPH(~R%rU=`ujpL1KFy5WO1M(@Zarha&wqJ6@zN+Yz;)Zw!ewE^#N$3AWuB|^qyKrABKMNCM({^oCi69OZsmYpLi5&J zQNP!Bq~&W5gLQiFO(+ts-%J2#A{)~~|6n#P4;`@mzzFiia8^ONee$(|;)q|alp z#}?5WKCGbSebFK9!Wd^Jmycra{u%0ttPl>OK^#%jxM|bU(!Co$AAGN`ubYO<`GqTq z#W)J>PnZ4R)ED%Jq~~tvKCkAAG}FpsL69irZLd?n#YpuQs|zxUR+bgD@U2 zBs6a{26!VqUE+)^b4- zL>YQ-953o{uURK+^5OiMrHP!z{RsvVi>CJSl^O$&65}bWttFD8Rbh+POq~$lF0d$) zJ*>DqT;Ijg{WeEI;3xT!DQT{K51#3{XP!1*YVL)?NYDT$(%eQm3F@W6@V@CU9NC3J zg)m>s0Cs~te+?RgpNDjsWZ$X9%5RMA*M2aKm#TN)q*Gu&(-bpS4e`KaGFU87I4^%q zyTF8eK`Zv&nk8f$8keQ1qJng|JGs*o<*kfuynteaASi--o0-0?F}cAN)rN?To^)== zOA|reeacAUONG^3Q(y5hK{J1xiywNXb z{Az{LzKD9|Cd#0b3CVb2X$wZln9(xiH&kB0pAi6t=w>!jAZ#ju1)5r+p)ur3GaK)u z-ND)&SqzAVCD5l(R;LHX=hM7>%6h3icwhNDv-Z*jZ2ISUl*_g+AF#t*3%2EJ)6B3# z(*o(}yL4`O5quPqs&2c(PLF4UiGhq}@o+(6L6e1WzFHg~CMFw|1C1OV{f_UwM-2v$ z-KrzcS5R<_^~kvDs^aDdBR_F8LmV zR9(fN#7bJXEVk$@RVe6#$>@0l6yKZWQb_E6vl*-H`-3^Y^}_wnM3apkg5qsZYiFC{ zMY|ucVYL+*t~hP*iAR`S@Xx{lHaA-z#I`2|Y^nAqKDQv{H~P|Ncp^Grs1Gf*dnLfa zBA@~KSy;}zy4gk3z?0dTW~#gCcCO)TNEQkG^Fu6$l}XpJW_(-=!4EAU`USNz4?Nka z^XGfd91LG{#W*|GCdO~pd))3L<61C2o>1_}zcP*|U^1?cJk_1@=vicRv}``r>4~xf z8tkJI`SF~)>QT=d9m`oXbJU4Q=XtB_UA1vI6n5`b-9a1|4lij?q~s<#+_`O z)UtdGfnS_p+L@GMZBM)2PXn(+h{x5-`>GR@im(j*f}C9@$Kwr#kS^h^u9Jsq9Qodu zVZlxx~3&1&O`aava6WBu*VZd&B!NA%k zBsP*-x*%2T$cuo=*3kyJmVTRY7*f?=b@``4^Ihtq54f{;+%?zN*lj$67a9t4gnph5 zh57`NbclF27Z~Mx1WbmDJQr-g#@snsc4nW2D|3_#2pG7ytr?dqd6SYk(6R%}-J~in z=%>JcyhN@RlswBp127A_P@HXn!$U~2A}n?EDLks%Azk=@l8f8gR=|UFZxThRD=p9i z3GsH6iC`rXZIw3&iogX3mOamZR{)X8i9O%-O1jPn?{h-V9jxa1{@xql0Ics7sz;qu zmkHAf4lXkDcE;7>;rK@u3`<3Jgmw|gOU4{)YUrz}mzbyUmz7?dCfsiW)E(K7&1^CE zIBf9G>lnwF`j0xBOBy){&=L?dl_q{jtH0g}(ilHwN6I8J;87OtFJD4}9m_xIWo8^+ z8n#99DG!vqjztJUO}3r*)nfbx5B*k1O~SlWafp4hKvmH$D5pOoC^4HmO@S`bzp;gXC1w{Thdn-5jG^>HY>E)20EtMaoSn2?sa`udm6GoJ*aW^ZN+V?%kMH?t z`>2f4b2>Hsr+$DhIx^&$(*>$Opm3#fIV6JQK1dg_)prb~g0$vrK#`~WzG?l9ocq5r zbeqD;@VLjol5S{rO_n8qyPy`wYB$$c@@{UBS73+aR4O5YH zQ`SoS;!y$OSL{Xe)a*S`?PDQXfcNY4C6+u@5v{R<#M6A_Rth#7y`5}-1;n15Q9au7|N1N!j73oV?G`7F zRz+bET^J;dB5WaJ4=CuwV?o_)r#b!=9z~0Sffb~xdIYAAm?_w$QSG9YS2mL6Y797; zVt!OreQvA#3px5XRF&@p>gZd)FOSs@knayx9Y`z>MdDd zQ4m@gIA>R&!8@_hrD>1Yx2(G&`>~v?Ksl59;!6HitO4^sAE-kJDkmN{QEEtPa8czYkM)~z3Dk<|l^>jiH*3N|-% zu?_!M(gP4NosmFhpEr8ah3lkKI`f33ldPRCEOl~cO*|#9pLTz^lgk7-pDuBu~?QDPX)M;)dzAugQ$hEsqHrQWZZD3S7 zT=`LQ;X7A^bAf=9XLy~ACkBygy`Xn~U=J^w?RLYGx=&?6G9=jtTFD&nT#4UNHBaqu z765&e_z>4IWlE#(0jTB@*SnRBnnshcg@H<3t};`2YK5RUd96N;4Cz(z&!3H9T@2Yb zeaCK0r`>5Aw|4a?o4y%Sa8DiwR3YxLC@e$K;s)`HVQ*Eja=IZ>@oG2icag8sL*D`w z&-+LFi8e}_TnR_Iu^|kX7iv1$LZa6sUXCLko+_HK4+r4uO1pi!@c&C6{f1(kOSRw5 z8e3~pv)fxycOIMc!NDV!lfn~L9|k2IPwpwIa}5smwM+=jMy5)TQhpAE+i-Ma$cgq; z5FS>i78XaGOdI^91_3%~|}rwda@ z|L_$8^HhHNCD zzN23Q^-aWSit?laLASD^`)IL(TRP!PMm>^C!-L5dgrhSatyUm!HfMN3_@(`29l-1~fHx|DmHxCFM~jNWR^;}|jMTjJT5)r6$D&|XL8u>9)tm-M*(r}^0)u>_`ilv6Ks0VyM5K84kiQcCWMLG^~l}4gf9(TyW|PHRQh)gr^bE`nu^e%z8Hg_Oxn@NPy^wsw`q9%5uz1n8H)~#tPYq92B)^WaqApeC)@ll~ z@}~{UYgv{Zib4TQSXJ8;dOC%eqkxh+By+aRR%vJpqKoeC*hFZgHiw*YH|yXT$*=dr zPyL^cIF(>g{d4}<+cxA$?$JoHIKQ`0X<*(jZN3Eqvx10_-2&4SKNYRTmKHp8mT9c` zysKN#+9n{{dt}DoIswAdsz{pF~V=D zGZz?2g-Md3MX!>MzAy=u(E`%$BBg{&$*1mQ0FQ%1?KOplqgR|$WQ3Hb7)>SOyDdBP z`O%IQwRNHY*g*rDs>D+_FWv;Y*5G)npOPSR*JQC{?qLbC1Cq>7wc6L=|wT<})rYwxxK0nTL9Ml`i|<_a{gmCn+%{Q3u#

    IOzGfyZBEY@v#XMZ@+hs2c~%#kZ6`wJB>$8dhe`53`&+$!x$A-hNvj;HgPcJlG{ zWU{x1Mv;0fgF|y_fnDY6e?8_CxD7O!5eMfGnK+g8i^glIDt36MU;5jZnVVys`Kr#a zsBVDZ9|Y-7sX;Oym^nY$Jq+mn$+H-afq=!kCNx3S7(owN7(0yJ(V?8Vpuhi7<_1HYMOPJiu6y3mtWAk zN|DJ3c!~n5``9Vx_znkpfG0YgUQS~abbh5^@J*50z&(%;PdB$oivyR+5M z!?kv*$AQktS+@zqg)};l07R4#K*I((X=K|?>v@r5zpwlF`(J|#u*4D-O*Y0m;3f}3 zbXFR-)e- z#eu5XP-$WQGg@kGhl#ajB;~%HE;cr{&LgyXo?Ypw(@T>_J?{n-qsmwFlTiC>%MGkI zQfoAQ3375Z5wS4<;YsO@!#txSKdbWhK+ysQy*&A!RjX1_q;YP$Jlzv^v(&7tiZ+a! z`v4RMaS!vRQkL&f+^bM8KLN~Oz-W9enhaG{{Zmud_vSneB!c^1cJvcRYTu zw{XgFL-X}TlcofZQ67KN`8i$C3d3^>g z7WX0#jiWKc$L>uG>pE7cv}Wo4b5+bQ{+&2QUwi@*9HtfP7i1k2orez>8&>*tVKzLp zOWiS&HV%Fng5ch^Vdb8dkhoRupB^oD)}8{l;d*!D^13~#V54=Oq4vCu!i@h=)~HXq zzo>;t!q5ERV;p#UJh^E3dhvCySvlWkRBQ2;p#~!qt0ol2eld&#!bYzT7Z!<2FEb>} zC(a0F+iW7mbi~T+evT(R&5+zPHSO-6emGw@<*GV+d_OiEJ&6VCP zEi!s63Z0a~$WK6x4&gSH!>#cPslb8*Q8mxg$pM!0gXA%^b}AgWpk zA2HN{aD8m{zwNPNk=(l;QL67S2*VNgdGJq>x@kKBu8IBiX=4NZID>iZ(`2@(c>0G! zrh?{2+}Ce!`t}WcEPwbWou`wDv^%@(xF^o!X#L6>jQl1$CyiyaYgs_dGJ z@)^~dXulH6Iy*m~Zeo+HP9BTR?X&f<*?LpK80M$5MECLJlz>^ih+U24SKCgf-r-i3 zzeuKjOEE6Cnz4woH4H`u33Q_dH+inqW_k>5HI>tBxN}z#_dMl_xQZ-1e)4v z+eSt#*oF_mqg9;xc@(-^R%_WhAVvvJv{qXvAHU=WoxGO8*it|`nW?dnopl)m6FY3L zsN#P2ZN4AXE>LUAE8{zZgBy)e5~2IFm1{Aw@}Q53jOab6LvObU-1-XI z5NF&UMm+{=<<`>kV($yDwnQY!LlE@f_^jd>kKf4=%EBXam_F|Z=Vf*OB0Ix_wkHQ= zHODC%5#oB~zI4}6JRdS4n@nUsrPb~9%<8ngQ)*gunT*T7e@rz;+Qf)|LGmN-Zy>Ty z8qk{Kjid0QJ3=;-0|5rF{&}X%$A5je9s85)3+DcNGfVeBV`jFOjz!Kt zxsg=5eI0JPYB`cJFX}wtDJ4zF5iP{gz%j;%?rf2MGkLySH#89h1QNT>y<=2iGhi&v zhXZ?$r2GlyIu@}s`G=#NprB?$p~jmQwzZ7K3)^;5!aU&<{6A^mqlRYc;V>f-iX-jSQ^m7@cCT(*)&JTLg190Ls zaH~stm3sFt!6PlZH$Io%&^!xuk(txuI!}OX@<>scF6cK&iz6U9fA}83hy=OH`Fo}e zq>bKZB4^SnjV)`vGc;PfXpp*l(Y1s=65b4km4M?wFi|65)?bqgGKG;k5)H!p@}ac3 z-!ki@bEx%71Aka^H(>zoU6IY7?~vVCkT?QW7LL5Bu?JRr3#EAx=5+)NwI+MDcAh@Z z^r69Kat6DS3|t+iXzZ?8_v zAdCVY2eG1JaLVP`98GA{c_^g4?Q5ucejs>SU#c)6o9iriMjkauN6y?+JD#HQjvmX{ zT|$-e{%2^PV8f!@WPs#`7FARx`!h8s=aWWqakL6K0KIyG1_1SY=#(3S^j$|JRe=P; zMm&$u-D!wlQ>xEFgKB={L$^Bwb>~^|4v<|$CxO+#hUA+Ua=Y)%cK&Rs$;R#~pUZ~! z-D{+^ii(Qf<`VFD7iFZhRb%>ns(?r75>jTffZI+E1X{u0cM%SH&@%GNrTmVU5J@61 zbJDCB#PR?T5OmNV88TW1Zt+D7^qL~~SJ}2x`4dgst??7Wi&!hi)=_BzP1T1HppUi0;4P!U>Z^;gL2)W5Ir+Tpj)#f=sXYQdG~J3V_76i^GwkzJ2N zMefbb%`IGX(!|T()+oYP2%c%Brl2rnkYlk`90saRNdi^c{dKLqAZh>}ljfg^A~VcW zZoNEyv8|Oa7v>~v&enIbdEetq%}8N#x5P&~ocB+UuoYtem%Dn57zg#gqye z!t*m95dGk1scVA5Ha|jGbgQM`I4m$v8hE5BZNmzj(GZUKA?Gpwuh!W>ZP{z|TbDEh z>1F*ojXno%&#X+;%B4PStAE%??rOT68lZ9CE0>z#=v%3GAJ`ep3ETu$t>{Tn^7Lr; z@XPiJpfZ_qRXsk0Z}?oZBq~3H?4%O=SbyJm^Bsrd(Y1(?z487T8Z!}v$|P`@5%6|L zHBN2LC!d#i>fn>NW}oAldjj`R1*J)8YO)vaZ*t6Uq=4je=7EM##_8j*q~pg_9CT$0 zyzzI@x&Ua)tOOt4EHrP|)6f6m?oxO?8=%sR0*SdkxWR0A5s&A^h}@mUDWSBJPhT7S zex}PrmNeinU73r@9u4V$f03&sa;F}$EKF_WdmI8J$DV!}C;$cF5sy)C1Acv86v(@4 zdCs?xi6wG7tSVg2=rYK+`6CdQyBHb>Tj70fbG;l!#%v+ZM6qGs7fU0@ybf3Nl|bJS z^rzVC#DM@@I$dR@m!}VmNML}Ltyecz^FeFYPOKdi`)QoQD!qjK?@f*~3(Q0I5`BH{ z{oM_6f5dG#rv#h4g}e(rgV* zi+B0xDlvaLo}=fv$S%1AwzKs|0$S7%os-ereWvf37Y+dZbixL1C(XH+VYJchgV765 z`*=o;O3RIW`Q)rObApT+z9-?i5?kWR%*Krf3;?F6+HQ7&f1>q!ahtpikQ6R~139h& zpG9}ec5xFRv6id?850W2dp}r&r8tVtB50Vmq~g=RXKxMS)pGj&Uer3!i)hMNV|CV{ zwWfF5Kln9#o@(9+sQTU4mpYf9$C>G=bYa9u|2pg>4|v|~jsS0BPPR8Y8+RgfDfAvK z=4SRV08w4^Vo%{gYI|B6IXq18%{*GBcOHS;f|ggOwQ%<5c&#_{d3#@@W2)FJXER?( zp_$o=f5IS6^C!%llgqIfn0Bbf>n(08T3pD}0_C#`QTYw)~&t-{)R>kM*0=?^z9)F{=z$xBa4 z4in`I3QtIETSJb(0AnSYd7)@NCs$Tv|J3D|=}@AJU=?AdF_>l4=%9>$n;b26Z8K4v z-2aVd$eF#XF#N&qi$B(T$^fz(W4+MKBI;U~bt zB)-Y#@|0%a#=4fgFiIrsAj`%9L2YNUC@wR&T*r`%ps!6vE3vOsY17Z{m^1o#XUNhb z)Kc~xX1AqInVFT5Gv{iyR8&teT&u=9T}_i^B{~(|Ce><jmNdX z4-;e6L}o`TQ%Hy9+Dy*HgAIU?G~&Ffdj0)3w^oVnx~k}s@7Me5xIBe#sQ%6H`^)#= zx0d+XVa9}M_(}qe*HQd}+x8fvjosi-kqIe+2*EhWzOGAB!PQ0qcjFu?oh>#nTBrK? zr+?uz{&Es(Y(v`wkDG!LAQ{fX5gxISdvo*kjGT=q_Nv?%5)B5OMh->J*4Ig+>?)Ui zD~2$vo)f&HwOF*^rnGn_ty$CrO_f&4nC72XUDK`gU@QsE4*~lcHm#p!(+>$RRC{a@ z;GtiBOj$v?D#eAWjB&9gWL*&HkGQiiQzN)EHA%^CemJ<#FcaH#@mI!>e#!7y@Jb-U zy5X~%;bOvNPEDsGYBnuLA7Oeq5uRbh&e5zz~hMoV%Zi6F% zc3I^9g;Zg>WkaOYCokG{?U?mZYM6blmUnhNA zHcS(PhcyU(3~d7+uSC%cejfPt}6mzYK<) z86(gd+}yw9vA5OeRzr@VPSP z^D=wLwWZwhu%+e_;tNjRy0^(^s{YNT?Ogx2#{f0Uk?-JQ?{)imHGQ`InLz^0dWrn1 zHg`G4tBSUkix<}{fwvCLJ^#)6P{XMo1^a;O=4(r#&AY0$gt_MA78bpY@E3OO%khbc zgjChV3|ji`;lRQo^bp29X~>oj+COHepqO@%S!__Zkgy@x9(a=cdNr-5^ptamcOU`C zOYqrtGuaC^uC^avqKP9OqrY@@fy#hrIN0LMSM-u*5$=;#3x0p3l{*b zmJooZnb$AGwd4RJX8xlehyMALL+4^tVnCRCP}d?{i!SaD5ywAaTi%V#U~JxU4*)^Q z3b``RKNV=_UORr~X_zW^`M0VopjC<*@PLXB<-Ly+5d8M+rIYWaTTX%4$va-Cx9 zvUnvJ@aK6UNyj7)y&r;-_eD9NlYA!|vxhek`kwxJ7NIV=hl2T7j2gw{M>WIWp07X_ zyoUT92K^xqBl)%L#mLh;7Bksav~G8zDs3k?X6U{0J?}Sn7ay+rHi}FpzOL2l#QjcK z9vAulkC#%Is@)4-+*xEW@X(Eb8M4 z;T)BAw*JgmN;Yf7Q>zXN;kxRB=gso)V4zj0gN3ao3|yQ!en-5%X_WfiHfMj?TIKs4x7KuT%j;{qkl*?mFlMJxi!Xd>on?(v@}QYivDdq3@=fHPtTPXAo9tbqL(ez!+OTo}B$sAv zB1)1@!rz-$uWzF2rGd|`No;CXgQG-hEYZ%XJlD^Af?CAn8!mSG*5igC<@+mwvI85Y zqJHD+iMmD90^_#xD>QNz%UKWRy??eX-PY&)AY3*{_evuPF`aadKOXizm)R+HO^Mw% z>4@~YX&IT!6EuDr|BNMaEf2Z95zvdO2y#~QdBD8ZCYmk)v#}7`E@t04*r*p-Vlb9x zX4b+DF?}()F~>|hYFS#`+ zS%AAiw#SfwpW8riZ#+|S{>io0;*a>$pdh@TE4@~*vU1XC_y#n}m ze|(VJOIHY+A5)qeuQ6ovwT2Pinr@IV5jr9|jQ+Im2#+w16k;X!d{8jL_4~rXX*Gzu z&+CBq#{V%POu{KYn4pCq^y0A1C4{p@tsI5&&Fi1EU)b$XaV`3Ec&Y|7<=eQ>dXXsg zGan2USG6Az-Djc{%EHy7u(_~4KSayJvnL@e-ZekD!uyKvtNz0Mfaczk+u%h5xMG>f z!Mg6#Rl2M9fbLnP)Mi4OER`It@AfU}K3m$so`sqwd;L!(auAFA5UBwyUQdPKty-68 zrCgMcouw$D=(phaWtc0FP^_267010&GkjKQCZCZ*NUAGDLKCzM30YY5xT+LSQCB~+ zy)TlVUbsxIUEYA)xaXZ`AI@H*AmCYW=(V}_b5i9hEr!|ka+YQ*pr%dwUV)(IV|fD{ z$0Z)AI<==&U@~N|(&=3oj;XMb9>OZTN_%tvg>sHzn(o9-1d%vlM2hCbQq$SaXR|L8 z;sSq6oX=(9Y|O@=F(D+sCJ&Rr(WGIYdf2a9WTvx`u81k*FfmlsIN_TP_08$e~B z5}ntIHc8Af{&->gY3c#=(9kcNy6$|^9FgLb&T)*loqr35UpT#E4A1;qP>7>5d5gHL zbw-!(glEdlkco}`ck%AWPddW}OMJd9t*h)=-RVx=ie8++ zY|!|JW6>D^X3rLtK?<8ppzvkD4r4w6{{UsCW9L`XA>_lk*Y7Vx6R1cB2r#hK)-P$L zOIt2<6XAgKkqlN~Wm&^b1V5-OTfPG$sR|Vo8?gb9-t`9NLVv9|1VzdF@S?9gn^qk_ zKR`cb* zmNj&6w2W>K4*mQnp{FV?ZY=MOsG)_rL7Fz%_N6Eh%%Y}i=x1Gw_tm=4S;DBCeLiMf z2cK4pWX6bzz`!xx!gRIYBi#&}{W#t!A$j%;OT?YYp7gFq{`cVhN1ybqDeS@Q+1kA2 zwL2h~cyg1&5cHL`Cc9M!*=L0O6&GbC-&+AX{ski;Xvwa?kXvJ`QuC@ZYomLkhJWf^ zk?mBy%p2RO_ZS@}_@NjuN*~5XdE_!^+oimAeLqO?tyaG|N!LaBn4F#{+F(6WTrlTm zWhJwi@w4bpPc_<*$fW8DZ43AKIxd-yDb2F^_EMBFB)Gk#;Q`)qmLe|-)QbQ5Xfim! zib&{6iXU;1(AX>*{nxV~Pz^qM+ibNp6_5vVT&7eh=~KHtdJ#i?o(@?N_v7B$j=-q} zPQaJB+IihZokmn92o_cnJ=pEGWpqPQVG+{OhK@0${;=h29GJF|7E|Ra{D@`&f*dDEAi{GjuB+z?tR_Kv zL7n{W*o%#JO}Hvmj#EAqNidq#i%x?aX%jN#W}HgfGEqdVVmb<8Lz%ky z_C9<9YSsEzD5o-1G+kY^5)n_22GmbgZhwbpu@Xya`i7NNvrwbPL2=8Cra#cg*}~_W z%t~qoq(>vzzYO|#SLkn9%RMFbR4si##nOB%AQx%d85v0Pxw2hx8Yze~s>w1!*lF8k z#4X!?7WrPM!TJ})aLenc{{@lAAFz?xzWd)VcM8yeaW+6n*+Kg@pr#b z{|%@8-~TKFa6cSYzKN#--bWjg0qJOTOvO{o9`FyM^?+vWA8{CR;kgK+ z@!8ggK;Nq#D~0POY8o2JbieywpQK+2&9E#o zkyrVru!LceT{zi#%bGD5l@>ph06q4W=x@m8$*{5C$R%8ipu$|k6D^=SXfL;xfxX-+ z%KwIozc!ipkNC#ExL%OI|Iw=Ewa2=Dfog5woRiM4S6eV_Pw1i4(UXraRc-csO>}y; z%4!J3dKC>5Oj?y|1US}(S`i_ z(TLLQT4KPZ^siBMQ4Tz2;(vQE$XA}c?_CE1DsFVuAn!nk`{9CLX|<(-;xTg(T3&8$ zPpZhKuHwF2B15%+vahhPqSk}yCKP774 zbp5VCvnB%2tZ*e?4#65-yA1NbEp2R0JJcs@BYG%uu@@45uCvUcdK~>kt6w}eY?Zz5 z=AgkzD-vRN$1gd31@Y$<;FvNAK#Gzqc=>0wR3lDDWH14PnCE&Yy$CmifUcP$U`V7u zP?Uk!oY(uUiDT)gs8<^nr4NSJnwU$1IdI;jKur5i*s?#Wc;B%xhUFr5s@x*Tj z(t39QbsjR9nHW#_9}8eUXRL&T=FOYmy+%a`A6E1ZuXI+z47}nwqQ^n_F6;1Xlmd2f!DQJOwtu1MDyMp7Uuf)~})M z{PrvGK0)9hHfkY_IafCROwYfA*kJ}2$4ZPILvg)R$FlnqT?rn#RihWga-YAU!3B%6 z=kOSft;HreZr;C7!N1Sk>Rpk*X7>Tu3?gSbY}B;MHo=D*YGi0@K{9k@(KH7@*LFSc zck_F{PiplhwjM@zN~x7p1DCg=3)fL~yL`Z-?3Oe9$tU}keE#~@6m&VF;>FeW%IfJY z`H^60#o?aXp?#moUI5UyR%KK-JKC2h5AIWBW5Jr&S6ES`cd&%f-|X?2=+-lGxHw0- zRu;DBFH1$=Q-6!IvX2_8lksn89q6jbO5}w+ls{2})vn(I%L&(Q*U!e~n4Gj~<*~uFZzpXlP?x>LeopkhAtx=u9$~`ZYOoa*K zeyzL0bshG)>x=VcOS!r*z}mF?UN)xZGXCEFB>TI(!j(HoNL8GqXoMz5U%en?~Xq8?MmLjuHfR5||8 z&CY^PGWjEmcCx8~)yU3WkZ@E=rS8q5>bzDI($jU4j@+Z}C6dRG?dq5g1?AXz9H3SG zj7p!I{VpUYumu!* z($k%%s+cUvhp?fq@PUp=lGgC|St*?i!f1R_Z!c2Ld_iu`Hc6`HOxyp(yfswKIya2|$$r11_y zdrR7UPQCv1(f@&3n*o?z!{P5jh;I>gz*DueIT7fGrf7dtsLt382C>C{BSxjtekG<$ z0r-xA;~^tpz|>~qwtj!k%Ndx6?RvwKTg|soPya+jjDPnx(0i%KDtI^tVB=5Xi)`I$ z?G_n6{ay#>_U%mu$NAXDHM&RffAL9?~SfUM@~8=nxq$u7BEay zSNaA0KdN)Y;P{?r+8m6YjbrhCOH^lKHXNY+Suo)Dp|c_lug8^ zXI2;G!tnoBmM=Pi^Qc{n$w~FciCO8696-Y&73BMlS<@;NMG_wr3&q6_fKb#2d|bHu zi%!Gex5q$8JN!1GSZy;p_5!l{%G z3ZJ?@u6?NK2x4?Z?kG8hp{iC=XW_<_dW`}DI@S(NBxg|AyhZVaiF?> z+FR26m+M9GN|%SndfFE6t*%8alBAx9@1yzPvov8qL~&!StEKj728uHx4<|kw(aI%% ziuCk^A9je1e2}M`wpwXM%l(CnUSdM-?vZAJBm3s(r37AKeb1!y0uj9_Je>P^kmW3S~Abe$Lpojc;af7CGNk`8d19zcT6^%*aSsCPRApyTf23S8C zj6=`KsEK*(E^?{R9lj%L(<4iHsFi<)8j(WdZn>aZM7*v!he$pkbL!TiYo=_<cNrfoF11=nC3NN~5{ z9^4&*I|O%!;K2#*Zoz`P1h`U2mN~r%u(WT0f|@7i-q^^i22k zTvxXm{gcNqTf-4Ah4zMIid7Yboz+ytH^Dtf4y*rc`~D{(-4bpbT@t-^25dT&)|P?i zSiXAY7hJQ=`tb+`vt7xE+5_6?O4JwN7`o9H*PRFXuxf2^TuE1QNZY#=RnE0nXx$#a!AcwYAyPzOmDW9q#lov zQS=-xWpZ{$iz{{mnaO_&~Q;yf|Fy?fPs(Z>=cuNqkn*Cosvd*D3MJr&{(bCl7Y8b zG98)vXx4SkKA&CMil?}EfTB2!AhyL10j_$vJC~qD`tM};^$fJ$=)@`7{%6;;P_0q- zo{LsR+b8xB$cZ@Ih3*6oKUcgQJ*^5er}yZv>1{rhCdG~_(A(Z8=`yR9MWT;(m&mXC|{WH1CINxF8y}-b!&OKEw#%} z_$mz|O!H#Hpq?qkx8UUuI3W|vdWfQul7$keaZ=y^dF-1 z=qy&AOIEl&0_+n+D~h8!OR5Z+v5@~(*jtQm$Z#K6r@UhkQa{UQvM4NN`xcwUm{IT? zMa^vgAxbFsKA6{jPUGgWj*WtZXbPxb&(Ir;+Imu~pOKWslE z<77R0gA*r&(&=nJH#lfq@Jnu&J5C53Tp~?{WTjU#1yWVy9u(-y%bT$+u zG8pCjVAf)>esA7Tk-{*ss-tE|$bl zw^bU5S`~>~-+UzUaUY@$#6>GHyvZ+6u@m&KO-p37^RIF+20&6NSoilGQ!oAE(J=QT zURD{m`eJd|4*yqT|F5uYDZ)`pM!F}>7|-!#A43)s&0=_epaVh z-5gG-x4N|UGY_q{foWd9SAI$8(Nk&HQvRV|S=Nx32SWB^`?Uc9u0t0w^4)Swt#U&= z$zOZ<)t5z5a)<`@tBsGPHfx?*BPk4IW)a@>*$?UC6Ymcnjv7huuHni_we*lAsxNCr z(u_}3{1|p*KRm3~2&6V@h5kM1*r1hc`E@pdOWHjgPd+ai&U z)>;U9{oZXCy-G@pCHmpGzUL zuSWStD1V|Z|IczkmcrqObqn}QKOsSX6v#N=??_(>{%x?O$dLbpeFMXOG5tMial(Fo z5&(4ZTdnKNR|_{TcsyIo-4%KkaoH1sSx;C%8{G3n&pZ#_q1Jv+9nH z^Q~2@s}p+OXTB#95ys*oYBu}E-S81#zv5Uy=S%&R?)0Qe%ScFYPVi31*ro!%zW5FE zfHTv0*e5dC&f3p_-@YRLa|sh-#6icyC*?Bya}V;naPgnpw49Yr5&!=`Jycm0=yo*6 zNSQJ+F~OXq;O2Jk_51o@Mz((j3KQe+E8vh)$rf3PE<*jMzQ9brILb_}2uzBuh)95T zoq4o&qfJ`4ikyn0d=WV`M6&-}alFtCGEj?O$}5486F@s&mwm(9+S<%%R-+!+tD*O? zsp{n#Ehaz23KBmF!~T7#$meR%kkJsyr*p7AKisfCKZ3a+2<>QEvS=c%kG)~&Ty^GS zGMq$3)PJ=GX*tdqdIvpQ>H>~Fp4f!HanYQJ-SF3&y}%H94LiFkqvcLt@0k{73vD4f zXbs1|8~l`;j#StI9e7iqTp+RAycNT)*XrUJ0YCU;0BI@HNu|rrmzI3i=W5^SpYHu> zkRhPny<2o@!6L^O==XTC2cL75(Y>ed&jNC(-iYIVW=CjPfCY?(3=%%1$`er zV2K-Hk+FDQn$|h($kZJaXQWp)t?>pBq+xzg2iuxYs?b_T2rto*(Y&J}YSuD(UNB9N zU$KvQeD~8~Z1bnZMD{7%_2XGrijsl~s_SpQv-C461-IcB+jA@Pgk;9i_P**AeRh)< zaQH#psDg^{$cLo6;hQ6_RBOBDS@FAhGkmJTuRiP>eC!V$=-6FS%0pCcK2!I5neQUG zwp)zMyC+agx<;3*X1tu)_kSmhARBk)aP-rv!t!3n1<9T13yi)Ma-LJ%?bi%#W1-IR z$UK+q6%_6A172aT>N zPPlP-z51+Y?ce~}vnEh-a`{(7`Kul7yIh#UuOHN~A-paqbL^+%x(VGx68DuUNQpZ- zwue7;YuB29a;asU*%ZubAqf5t2h=HEgE1uUfA)CV+wSSS#|&5!76#kemQwCuLR(dS zJvSoab}+P=qsMvcRVfigV1&lK{>FmDNFHP0+gnop1!B+b?+Q?PlG_lex^3L$7K?U1 zdHKlQlXc8Xc2dH9z%?ur0}iRawoO4O469}} zf`h8U#R^%6gnKT)-x6@9tF5k8?2&;3qpwl#*oniE=cdOBsj)CiQDI#`bD^JANXg@p z?bW@qJPrzcCVN9UgH5_#9?Cr-fc3F?1OJl>z}JO+nI zoZ_-{kk8R7mh>}=Yik;-b>qQOi)#4R!*@SeCBb~0xG+-B9{$_kbxghPYhE2c>@Cz~ zKegI7MYrtT5@90|asd%cUNjF^ya9fuFAmq3MKMQz7W6MjcM|EZZl8(-?brC-wdP`J zczKhnAm3VSJw{_-1Z~BGf_JLHch+9F=zZ3)`T#KX+8T%+f4G34-3cLOwq?UIa07|( z{C(uOZ-=RKxIy|}dn)7*lsjA0tMEAgmYNtk_vIYlS&OaN>-U$av^wAuoY0`|P`tGB z$&#!2I3$$%5X(Mx*2Nszg8CsHKGQ)p{dp#Hh$hnj!sU?lA4_leZZyItwK!zL<{H?x zTo15FrPR2nu)Z9~uy3O{>0D-F-PymvESN4%rFn|9$#PaStd!PppcRweTZ@SwS*c6% z)gM9!U;MV38QU~S{$w`skc2O&T4(;cy(aKIV1!%BCtDIeL=wqt-G^UL*^$oKrZ*I| zN_C)%ojyUsY}5EV1VojS6_fBztRrV&KuDe*|WG%k?OOh>Bc`QCWe5u_Za`jn@UV#dd@ z+`{uqaa!pRpF3qo`x{8fcAKA%8J;&c-$UWI7A?@|c3KnmIjz2~T>a|Zt#dR;{^m4b zMV&FDAUL(+G}xhV!DA!r?|cGb4N<%mvdk2E+E=y9^*-O$AX&~(Rpjj^_T4t>?zMYe zLjbTU3OxoA5%}LErDX7(c~1J=_G?z<)tj|30*RP~@FUsnU!bqH?Bi zK?u(+zYRwWAKXE11uc-{kTFR&vQ7Y>*kOG2_6|A4`m z2*T+Z`s@-UICwSI1i~Fx<2a9d8Se;UWA6-x_b;i;_nj^;`_;|c_FAR0(%}mdi%qiR zKl|O50wFrr$xo0jCk+i;?$86FU}BsGNj=w@N=(tb?>Xn2ch{rm^{vo&_J3(*My%8c zI*I3`Bp^JG(TZ8T-n{JvTE+6S_W3-^ZwQ}k#qjhzT}`7rQ@+KC64)o5Fh=4owBPsw9Yx)?zS7l?qwLR* zt7G2VZA`2d>@j%SFt1+>VXW84vN$|PGIxG^HV<3cy42Y z6Rxx>zeCJMOAOiV1ozfkcHak=D~P(LSQcK`S)6la$t(Bp`CRW$&2iS4mLMMqVTRQN z7;Wfu+o)>wy%Qr)yvrt&J0YcvV*k{Phn))>GC&nHql^<6n1KhXfI;W4{_V|5!1M*s zf7}6Ha|X4ndsaoau_Dhjf&7l9`&YRcI1cYmr=-x4LBf}x2#rX)d3m3&XT~5+n}>czd2=SD=&oMuxvryX--iZWY10QZrh_qwRH(J(7FDiU|sQk!Cm#g`2eyb`R z94&}O7?ak^NH+}Doi!GW_)*$FBogW_KwhOwP~aJff1Th zp6mn%Ltm=xw6(muV?>8=pK{%Zgv(CXMv3QCt1~Ptg@IXIP%T&tNxHbCqg}?Z9YOSc z;dAoITbQrP!j@|OW<+dl;Kic$4>DO6$K5>mx+?ibr!WE+*Ur16KL-X0_mAFtX;_xN z>{Ou0a4`5hUvljT#Gc@ANs%oga=`@OjH+&)Y$R@jaeG5&k!*)KZNG|bX=jyF%(wcjvtb<+AgUw{-iES{gS*sp(t z$-ecQ7-eo`i>bXK)X)ezP`l2ij2_`wZ2=d+|87xJ^s5esNtC-?iV4f7PBox*;eoLCytW|BV`@bPEvRKP7#Y`hd94Gzs) zeSR3fSVGRSMA4nz@9<{DO+pNX-Ax_~)kal7j_eNDnOD&3%O2tJOD1Ug7b%ZZVI)zb zM4pQSQ)*1CWEze0ZyrE#1tVDSA_&z7knPN|t;`ZD_+TlP@qxxAI22dFpA@h9VGHo8 z^#z{Sz&-H@;=$4-x!-j@9G06y)Y3KLyW)&}%bBO0r@mGykId$3ld_Sk9PF2L;o&(0 z`Mm-C9R2zrbioCi8u1M>eSH=!K7GG?GjKO~R$b9$3YZXC-?P)dZ5#v;WA z@24#k9-nn`yU{nMUfWhUT~_mjQW{X*0$cf`KchI+q?#b--e(JW9Y^o{A#n^Wfx^@L z0RJwA`&l~XlmS7lAVs164eUKCv;}pzELZ0pr6W^I7 zJgHOMVyo=%?Do(0-GS80faQ0nX2m3=uP5%iJM$9FKAn#*1kQcGpv=z%+^>cQ8aWp3 z_%^Gd;L9=13NYOU1<;$<91~OL*9f$Y$%N|o;gVu!UZ1Gv=~tSS#d<%y(XzWMAB)d1 zFdTg@rWft%d`#OFKjJW?G*o&h@0CpLdfutWLJc{jW7c^0QtgoCvw2~jlI6uW&&Tlf zH;>E?T*}i7|J9wpZEuz14;#;&phvT?6Yu7MoE*AlHn5FZ4t|cZ{SEiSw}4P-ghtEd z0R;zT?x{D(U`oxWWdu&mSCCn83M0%7=jrHAAiJ1>%LL5C@9$ygEMw^(qeE`^I-lgw zQQH8;&-=ylp|s#OO~L99CoPhIz0FXz2+CLrQ`jTYGGj#)~|`e zi5&)$dQ@;Ees|i~#ira&lAxDC>~ZP7u2N-gHZp<|vl7)VV4e8Hs4UuX#kkWp!bRYB zJ@nq?&~pa{4yGJ;1UeaJx@1azE7gmb#ZXx7vWkg@=q4l#M&K3L2u}|y3mfgmOyf)= z|C>E92`+nux*j2Qn%h#~`~qt+o(lNXA(|#dWWHjcTB6`u<(GI5QT%V(H&T3-X8HVm zy@X;UQy1Y*M@QRz*orPDYQ3J9$#oDOEDs8?>*$iec9@ehYOq(KU@6}zBlmkPjsl#x z+EX2T<=0PSV~B;(&^=+Hu~5Z+yVBrYV|}2{@m=cq=6e5-p|jjb`%t?>=3V<&)gqs` zTfQ^Db!H>9z9$Q`uUi#kc^*hy4^VnkL8c0dj3~g8ZA7(K7NvE zD?sIL&S|*?X(l?6Z<);H`uWbdgF430I=s?n6nI$O?irVEo04-+fRM}-yD@ojpJ>T4 z`47*<#=2fTr7Hv-L|V?qb%0;r$GSUqk-pl7w+p@euB$<3jjmUHQ6q@?wQM&t);e*` zR+8me6{(Z(TKvQ^_H-mPpOO}mpl}zo*lBS9Kje_mu zQNU&55%9&`%?h&oIFd8qJUS|!EBx6dED|(pn%Te9%X78 zJyXjsVt<>wQLXbkisd)UMvvL2ldO^?8a*Tt77^~Fu3W39Kx~ z(xVBKH5<7msoTWV%kWG$I8&#L!G-My^Q?hBwsYI1}9XAn^UD52m-1}Y+(xEag$+3t@I`vtMUoR@2A{7f)J`}mY_*7hwk(Q?e(vp9(B{|1sLGaaY7*Rn8pgfdT_YVH%WO+Wv z$xLLp08B(4ZRlwNCB_%klsgrXyFP_j@)j}L2S~Y1NsdHYhOv7#FrmUh7r&7iq{f#EpxkrRi?oIhjcXBjfH++S0iz%8G(9O?cj!+K3YWEK^QYwD-#pECNGdo>e zzoENqziW?s-HHi6g_7vL0k?Mv`SLd+fRBhChq;q!j4p;9bQ4e4Yl0qeySRJ^pChMt zJB{V!%YsAAmF~|Erb}-&z15%3$`+;#qrODV4CP}+!V;;kQO0d?e7Q%1g)4#u)4c>D zLqqBc?%;5dZq&~xM7nh4D<64}Z_(1Mv5BoIA-7L{E7r{D%Dvo}m_fW55zE&N+CrC_ z83bF^_J6_ji+#8f(afbeg7-A{2~AnQd17tU(#uaoAIHpLD*(ppE8U%XiN{DZX|4ns zEtEi=B%%Aqu-&tA_o1x8=+nMGR+c1DFMEOnAAg(^#F4R2-;RV$;G6Zu2i*jC_tA@r z(BoA3zxbYP)r2HLjx<9J3C6v5dI5X-D3^PSh>>RwAxT|fK}HZ)q?=_4Kfr}T>B+5f zL-#^d2jup#QT*fEL@FW5|)-+HUyOnQ)ChGs6@v$hHhl^(`}lLVid%9 z^dTu$GZx(VG_M(q71fO&D1d?2qs+3v5%uyDN#mr|Dh}&;-0>w$8;0PS2(Hvt2-BQt zpDrUC|1eTyU`SzZ8uKhDQoOhr&aE5XO3kmCt0#~i(%zj`Gow>Gfp$oxXEtmj6zYPs&Q{4s=`abpRu!g-urug%3t=Vb>!(Z*&_LCuhQ ze;Oz1l<1kbU`lH?(2OkmoOEzC+?}&yoq=w&>FZvAJ2Wbz*TTDIS(7*kt@93mLW;AQ z-b8FTxh}t;77_+dRxNDEcm5gS8-Jgsxaw&f<}AbaoFi-^{92P9dtOG}fq@uQGM*d6 z5twJH1cP-d!MHq|;z$}~XNLoC}p)1IWN%zzjqk5Vd3$>_G{sfSk_^uG_D$ zJHcUYRcDG=opE@FJh3)YVU^JA$kRQzPI=^XHF7T6l)(lgnOVrSFt2>aajIvOW;+<> zae!qRcW>giT;xpMrDLKq2i4ucpndOgTv9{BVb=?u`RR7}nHk{w6plpS@uI74>)Bb_ z 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

    w<5 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 J%2~bJ{|mfU&-4HQ literal 0 HcmV?d00001 diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go new file mode 100644 index 0000000000..35c60b8a63 --- /dev/null +++ b/cmd/tealdbg/local.go @@ -0,0 +1,487 @@ +// 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 . + +package main + +import ( + "fmt" + "io" + "log" + "time" + + "github.com/algorand/go-algorand/config" + "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/protocol" +) + +func protoFromString(protoString string) (name string, proto config.ConsensusParams, err error) { + if len(protoString) == 0 || protoString == "current" { + name = string(protocol.ConsensusCurrentVersion) + proto = config.Consensus[protocol.ConsensusCurrentVersion] + } else { + var ok bool + proto, ok = config.Consensus[protocol.ConsensusVersion(protoString)] + if !ok { + err = fmt.Errorf("Unknown protocol %s", protoString) + return + } + name = protoString + } + + return +} + +// txnGroupFromParams validates DebugParams.TxnBlob +// DebugParams.TxnBlob parsed as JSON object, JSON array or MessagePack array of transactions.SignedTxn. +// The function returns ready to use txnGroup or an error +func txnGroupFromParams(dp *DebugParams) (txnGroup []transactions.SignedTxn, err error) { + if len(dp.TxnBlob) == 0 { + txnGroup = append(txnGroup, transactions.SignedTxn{}) + return + } + + var data []byte = dp.TxnBlob + + // 1. Attempt json - a single transaction + var txn transactions.SignedTxn + err = protocol.DecodeJSON(data, &txn) + if err == nil { + txnGroup = append(txnGroup, txn) + return + } + + // 2. Attempt json - array of transactions + err = protocol.DecodeJSON(data, &txnGroup) + if err == nil { + return + } + + // 3. Attempt msgp - array of transactions + dec := protocol.NewDecoderBytes(data) + for { + var txn transactions.SignedTxn + err = dec.Decode(&txn) + if err == io.EOF { + err = nil + break + } + if err != nil { + break + } + txnGroup = append(txnGroup, txn) + } + + return +} + +// balanceRecordsFromParams attempts to parse DebugParams.BalanceBlob as +// JSON object, JSON array or MessagePack array of basics.BalanceRecord +func balanceRecordsFromParams(dp *DebugParams) (records []basics.BalanceRecord, err error) { + if len(dp.BalanceBlob) == 0 { + return + } + + var data []byte = dp.BalanceBlob + + // 1. Attempt json - a single record + var record basics.BalanceRecord + err = protocol.DecodeJSON(data, &record) + if err == nil { + records = append(records, record) + return + } + + // 2. Attempt json - a array of records + err = protocol.DecodeJSON(data, &records) + if err == nil { + return + } + + // 2. Attempt msgp - a array of records + dec := protocol.NewDecoderBytes(data) + for { + var record basics.BalanceRecord + err = dec.Decode(&record) + if err == io.EOF { + err = nil + break + } + if err != nil { + break + } + records = append(records, record) + } + + return +} + +type evalResult struct { + pass bool + err error +} + +type evalFn func(program []byte, ep logic.EvalParams) (bool, error) + +type appState struct { + appIdx basics.AppIndex + schemas basics.StateSchemas + global map[basics.AppIndex]basics.TealKeyValue + locals map[basics.Address]map[basics.AppIndex]basics.TealKeyValue +} + +func (a *appState) clone() (b appState) { + b.appIdx = a.appIdx + b.global = make(map[basics.AppIndex]basics.TealKeyValue, len(a.global)) + for aid, tkv := range a.global { + b.global[aid] = tkv.Clone() + } + b.locals = make(map[basics.Address]map[basics.AppIndex]basics.TealKeyValue, len(a.locals)) + for addr, local := range a.locals { + b.locals[addr] = make(map[basics.AppIndex]basics.TealKeyValue, len(local)) + for aid, tkv := range local { + b.locals[addr][aid] = tkv.Clone() + } + } + return +} + +func (a *appState) empty() bool { + return a.appIdx == 0 && len(a.global) == 0 && len(a.locals) == 0 +} + +// evaluation is a description of a single debugger run +type evaluation struct { + program []byte + source string + offsetToLine map[int]int + name string + groupIndex int + eval evalFn + ledger logic.LedgerForLogic + result evalResult + states appState +} + +// LocalRunner runs local eval +type LocalRunner struct { + debugger *Debugger + proto config.ConsensusParams + protoName string + txnGroup []transactions.SignedTxn + runs []evaluation +} + +func makeAppState() (states appState) { + states.global = make(map[basics.AppIndex]basics.TealKeyValue) + states.locals = make(map[basics.Address]map[basics.AppIndex]basics.TealKeyValue) + return +} + +// MakeLocalRunner creates LocalRunner +func MakeLocalRunner(debugger *Debugger) *LocalRunner { + r := new(LocalRunner) + r.debugger = debugger + return r +} + +func determineEvalMode(program []byte, modeIn string) (eval evalFn, mode string, err error) { + statefulEval := func(program []byte, ep logic.EvalParams) (bool, error) { + pass, _, err := logic.EvalStateful(program, ep) + return pass, err + } + mode = modeIn + switch modeIn { + case "signature": + eval = logic.Eval + case "application": + eval = statefulEval + case "auto": + var hasStateful bool + hasStateful, err = logic.HasStatefulOps(program) + if err != nil { + return + } + if hasStateful { + eval = statefulEval + mode = "application" + } else { + eval = logic.Eval + mode = "signature" + } + default: + err = fmt.Errorf("unknown run mode") + return + } + return +} + +// Setup validates input params +func (r *LocalRunner) Setup(dp *DebugParams) (err error) { + ddr, err := ddrFromParams(dp) + if err != nil { + return + } + + protoString := ddr.ProtocolVersion + if len(dp.Proto) != 0 { + protoString = dp.Proto + } + r.protoName, r.proto, err = protoFromString(protoString) + if err != nil { + return + } + + log.Printf("Using proto: %s", r.protoName) + + r.txnGroup = ddr.Txns + if len(dp.TxnBlob) != 0 || len(r.txnGroup) == 0 { + r.txnGroup, err = txnGroupFromParams(dp) + if err != nil { + return + } + } + + var records []basics.BalanceRecord + if len(dp.BalanceBlob) > 0 { + records, err = balanceRecordsFromParams(dp) + } else { + records, err = balanceRecordsFromDdr(&ddr) + } + if err != nil { + return + } + + balances := make(map[basics.Address]basics.AccountData) + for _, record := range records { + balances[record.Addr] = record.AccountData + } + + // if program(s) specified then run from it + if len(dp.ProgramBlobs) > 0 { + if len(r.txnGroup) == 1 && dp.GroupIndex != 0 { + err = fmt.Errorf("invalid group index %d for a single transaction", dp.GroupIndex) + return + } + if len(r.txnGroup) > 0 && dp.GroupIndex >= len(r.txnGroup) { + err = fmt.Errorf("invalid group index %d for a txn in a transaction group of %d", dp.GroupIndex, len(r.txnGroup)) + return + } + + r.runs = make([]evaluation, len(dp.ProgramBlobs)) + for i, data := range dp.ProgramBlobs { + r.runs[i].program = data + if IsTextFile(data) { + source := string(data) + program, offsets, err := logic.AssembleStringWithVersionEx(source, r.proto.LogicSigVersion) + if err != nil { + return err + } + r.runs[i].program = program + if !dp.DisableSourceMap { + r.runs[i].offsetToLine = offsets + r.runs[i].source = source + } + } + r.runs[i].groupIndex = dp.GroupIndex + r.runs[i].name = dp.ProgramNames[i] + + var eval evalFn + var mode string + eval, mode, err = determineEvalMode(r.runs[i].program, dp.RunMode) + if err != nil { + return + } + r.runs[i].eval = eval + + log.Printf("Run mode: %s", mode) + if mode == "application" { + var ledger logic.LedgerForLogic + var states appState + txn := r.txnGroup[dp.GroupIndex] + appIdx := txn.Txn.ApplicationID + if appIdx == 0 { + appIdx = basics.AppIndex(dp.AppID) + } + + ledger, states, err = makeAppLedger( + balances, r.txnGroup, dp.GroupIndex, + r.proto, dp.Round, dp.LatestTimestamp, appIdx, + dp.Painless, + ) + if err != nil { + return + } + + r.runs[i].ledger = ledger + r.runs[i].states = states + } + } + return nil + } + + r.runs = nil + // otherwise, if no program(s) set, check transactions for TEAL programs + for gi, stxn := range r.txnGroup { + // make a new ledger per possible execution since it requires a current group index + if len(stxn.Lsig.Logic) > 0 { + run := evaluation{ + program: stxn.Lsig.Logic, + groupIndex: gi, + eval: logic.Eval, + } + r.runs = append(r.runs, run) + } else if stxn.Txn.Type == protocol.ApplicationCallTx { + var ledger logic.LedgerForLogic + var states appState + eval := func(program []byte, ep logic.EvalParams) (bool, error) { + pass, _, err := logic.EvalStateful(program, ep) + return pass, err + } + appIdx := stxn.Txn.ApplicationID + if appIdx == 0 { // app create, use ApprovalProgram from the transaction + if len(stxn.Txn.ApprovalProgram) > 0 { + appIdx = basics.AppIndex(dp.AppID) + ledger, states, err = makeAppLedger( + balances, r.txnGroup, gi, + r.proto, dp.Round, dp.LatestTimestamp, + appIdx, dp.Painless, + ) + if err != nil { + return + } + run := evaluation{ + program: stxn.Txn.ApprovalProgram, + groupIndex: gi, + eval: eval, + ledger: ledger, + states: states, + } + r.runs = append(r.runs, run) + } + } else { + // attempt to find this appIdx in balance records provided + // and error if it is not there + found := false + for _, rec := range records { + for a, ap := range rec.AppParams { + if a == appIdx { + var program []byte + if stxn.Txn.OnCompletion == transactions.ClearStateOC { + program = ap.ClearStateProgram + } else { + program = ap.ApprovalProgram + } + if len(program) == 0 { + err = fmt.Errorf("empty program found for app idx %d", appIdx) + return + } + ledger, states, err = makeAppLedger( + balances, r.txnGroup, gi, + r.proto, dp.Round, dp.LatestTimestamp, + appIdx, dp.Painless, + ) + if err != nil { + return + } + run := evaluation{ + program: program, + groupIndex: gi, + eval: eval, + ledger: ledger, + states: states, + } + r.runs = append(r.runs, run) + found = true + break + } + } + } + if !found { + err = fmt.Errorf("no program found for app idx %d", appIdx) + return + } + } + } + } + + if len(r.runs) == 0 { + err = fmt.Errorf("no programs found in transactions") + } + + return +} + +// RunAll runs all the programs +func (r *LocalRunner) RunAll() error { + if len(r.runs) < 1 { + return fmt.Errorf("no program to debug") + } + + failed := 0 + start := time.Now() + for _, run := range r.runs { + r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) + + ep := logic.EvalParams{ + Proto: &r.proto, + Debugger: r.debugger, + Txn: &r.txnGroup[groupIndex], + TxnGroup: r.txnGroup, + GroupIndex: run.groupIndex, + Ledger: run.ledger, + } + + run.result.pass, run.result.err = run.eval(run.program, ep) + if run.result.err != nil { + failed++ + } + } + elapsed := time.Since(start) + if failed == len(r.runs) && elapsed < time.Second { + return fmt.Errorf("all %d program(s) failed in less than a second, invocation error?", failed) + } + return nil +} + +// Run starts the first program in list +func (r *LocalRunner) Run() (bool, error) { + if len(r.runs) < 1 { + return false, fmt.Errorf("no program to debug") + } + + run := r.runs[0] + + ep := logic.EvalParams{ + Proto: &r.proto, + Txn: &r.txnGroup[groupIndex], + TxnGroup: r.txnGroup, + GroupIndex: run.groupIndex, + Ledger: run.ledger, + } + + // Workaround for Go's nil/empty interfaces nil check after nil assignment, i.e. + // r.debugger = nil + // ep.Debugger = r.debugger + // if ep.Debugger != nil // FALSE + if r.debugger != nil { + r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) + ep.Debugger = r.debugger + } + + return run.eval(run.program, ep) +} diff --git a/cmd/tealdbg/localLedger.go b/cmd/tealdbg/localLedger.go new file mode 100644 index 0000000000..e49d009213 --- /dev/null +++ b/cmd/tealdbg/localLedger.go @@ -0,0 +1,214 @@ +// 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 . + +package main + +import ( + "fmt" + "math/rand" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "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/ledger" +) + +type balancesAdapter struct { + balances map[basics.Address]basics.AccountData + txnGroup []transactions.SignedTxn + groupIndex int + proto config.ConsensusParams + round int +} + +func makeAppLedger( + balances map[basics.Address]basics.AccountData, txnGroup []transactions.SignedTxn, + groupIndex int, proto config.ConsensusParams, round int, latestTimestamp int64, + appIdx basics.AppIndex, painless bool, +) (logic.LedgerForLogic, appState, error) { + + if groupIndex >= len(txnGroup) { + return nil, appState{}, fmt.Errorf("invalid groupIndex %d exceed txn group length %d", groupIndex, len(txnGroup)) + } + txn := txnGroup[groupIndex] + + accounts := []basics.Address{txn.Txn.Sender} + accounts = append(accounts, txn.Txn.Accounts...) + + apps := []basics.AppIndex{appIdx} + apps = append(apps, txn.Txn.ForeignApps...) + + ba := &balancesAdapter{ + balances: balances, + txnGroup: txnGroup, + groupIndex: groupIndex, + proto: proto, + round: round, + } + + appsExist := make(map[basics.AppIndex]bool, len(apps)) + states := makeAppState() + states.schemas = makeSchemas() + states.appIdx = appIdx + for _, aid := range apps { + for addr, ad := range balances { + if params, ok := ad.AppParams[aid]; ok { + if aid == appIdx { + states.schemas = params.StateSchemas + } + states.global[aid] = params.GlobalState + appsExist[aid] = true + } + if local, ok := ad.AppLocalStates[aid]; ok { + ls, ok := states.locals[addr] + if !ok { + ls = make(map[basics.AppIndex]basics.TealKeyValue) + } + ls[aid] = local.KeyValue + states.locals[addr] = ls + } + } + } + + // painless mode creates all missed global states and opt-in all mentioned accounts + if painless { + for _, aid := range apps { + if ok := appsExist[aid]; !ok { + // create balance record and AppParams for this app + addr, err := getRandomAddress() + if err != nil { + return nil, appState{}, err + } + ad := basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{ + aid: { + StateSchemas: makeSchemas(), + GlobalState: make(basics.TealKeyValue), + }, + }, + } + balances[addr] = ad + } + for _, addr := range accounts { + ad, ok := balances[addr] + if !ok { + ad = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{}, + } + balances[addr] = ad + } + if ad.AppLocalStates == nil { + ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + } + _, ok = ad.AppLocalStates[aid] + if !ok { + ad.AppLocalStates[aid] = basics.AppLocalState{ + Schema: makeLocalSchema(), + } + } + } + } + } + + appGlobals := ledger.AppTealGlobals{CurrentRound: basics.Round(round), LatestTimestamp: latestTimestamp} + ledger, err := ledger.MakeDebugAppLedger(ba, appIdx, states.schemas, appGlobals) + return ledger, states, err +} + +func makeSchemas() basics.StateSchemas { + return basics.StateSchemas{ + LocalStateSchema: makeLocalSchema(), + GlobalStateSchema: makeGlobalSchema(), + } +} + +func makeLocalSchema() basics.StateSchema { + return basics.StateSchema{ + NumUint: 16, + NumByteSlice: 16, + } +} + +func makeGlobalSchema() basics.StateSchema { + return basics.StateSchema{ + NumUint: 64, + NumByteSlice: 64, + } +} + +func getRandomAddress() (basics.Address, error) { + const rl = 16 + b := make([]byte, rl) + _, err := rand.Read(b) + if err != nil { + return basics.Address{}, err + } + + address := crypto.Hash(b) + return basics.Address(address), nil +} + +func (ba *balancesAdapter) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + br, ok := ba.balances[addr] + if !ok { + return basics.BalanceRecord{}, nil + } + return basics.BalanceRecord{Addr: addr, AccountData: br}, nil +} + +func (ba *balancesAdapter) Round() basics.Round { + return basics.Round(ba.round) +} + +func (ba *balancesAdapter) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + switch ctype { + case basics.AssetCreatable: + assetIdx := basics.AssetIndex(cidx) + for addr, br := range ba.balances { + if _, ok := br.AssetParams[assetIdx]; ok { + return addr, true, nil + } + } + return basics.Address{}, false, nil + case basics.AppCreatable: + appIdx := basics.AppIndex(cidx) + for addr, br := range ba.balances { + if _, ok := br.AppParams[appIdx]; ok { + return addr, true, nil + } + } + return basics.Address{}, false, nil + } + return basics.Address{}, false, fmt.Errorf("unknown creatable type %d", ctype) +} + +func (ba *balancesAdapter) ConsensusParams() config.ConsensusParams { + return ba.proto +} + +func (ba *balancesAdapter) PutWithCreatable(basics.BalanceRecord, *basics.CreatableLocator, *basics.CreatableLocator) error { + return nil +} + +func (ba *balancesAdapter) Put(basics.BalanceRecord) error { + return nil +} + +func (ba *balancesAdapter) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards, dstRewards *basics.MicroAlgos) error { + return nil +} diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go new file mode 100644 index 0000000000..66a3d4ad8c --- /dev/null +++ b/cmd/tealdbg/local_test.go @@ -0,0 +1,927 @@ +// 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 . + +package main + +import ( + "reflect" + "strings" + "testing" + + "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/protocol" + "github.com/stretchr/testify/require" +) + +var txnSample string = `{ + "sig": "+FQBnfGQMNxzwW85WjpSKfOYoEKqzTChhJ+h2WYEx9C8Zt5THdKvHLd3IkPO/usubboFG/0Wcvb8C5Ps1h+IBQ==", + "txn": { + "amt": 1000, + "close": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", + "fee": 1176, + "fv": 12466, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "lv": 13466, + "note": "6gAVR0Nsv5Y=", + "rcv": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "pay" + } + } +` + +func TestTxnJSONInput(t *testing.T) { + a := require.New(t) + + dp := DebugParams{ + TxnBlob: []byte(txnSample), + } + + txnGroup, err := txnGroupFromParams(&dp) + a.NoError(err) + a.Equal(1, len(txnGroup)) + a.Equal(basics.MicroAlgos{Raw: 1176}, txnGroup[0].Txn.Fee) + + dp.TxnBlob = []byte("[" + strings.Join([]string{txnSample, txnSample}, ",") + "]") + txnGroup, err = txnGroupFromParams(&dp) + a.NoError(err) + a.Equal(2, len(txnGroup)) + a.Equal(basics.MicroAlgos{Raw: 1176}, txnGroup[0].Txn.Fee) + a.Equal(basics.MicroAlgos{Raw: 1000}, txnGroup[1].Txn.Amount) +} + +func TestTxnMessagePackInput(t *testing.T) { + a := require.New(t) + + var txn transactions.SignedTxn + err := protocol.DecodeJSON([]byte(txnSample), &txn) + a.NoError(err) + + blob := protocol.EncodeMsgp(&txn) + dp := DebugParams{ + TxnBlob: blob, + } + + txnGroup, err := txnGroupFromParams(&dp) + a.NoError(err) + a.Equal(1, len(txnGroup)) + a.Equal(basics.MicroAlgos{Raw: 1176}, txnGroup[0].Txn.Fee) + + dp.TxnBlob = append(blob, blob...) + txnGroup, err = txnGroupFromParams(&dp) + a.NoError(err) + a.Equal(2, len(txnGroup)) + a.Equal(basics.MicroAlgos{Raw: 1176}, txnGroup[0].Txn.Fee) + a.Equal(basics.MicroAlgos{Raw: 1000}, txnGroup[1].Txn.Amount) +} + +var balanceSample string = `{ + "addr": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "onl": 1, + "algo": 500000000, + "apar": { + "50": { + "an": "asset", + "t": 100, + "un": "tok" + } + }, + "asset": { + "50": { + "a": 10 + } + }, + "appl": { + "100": { + "hsch": { + "nbs": 3, + "nui": 2 + }, + "tkv": { + "lkeybyte": { + "tb": "local", + "tt": 1 + }, + "lkeyint": { + "tt": 2, + "ui": 1 + } + } + } + }, + "appp": { + "100": { + "approv": "AQE=", + "clearp": "AQE=", + "gs": { + "gkeyint": { + "tt": 2, + "ui": 2 + } + }, + "gsch": { + "nbs": 1, + "nui": 1 + }, + "lsch": { + "nbs": 3, + "nui": 2 + } + } + } +}` + +func makeSampleBalanceRecord(addr basics.Address, assetIdx basics.AssetIndex, appIdx basics.AppIndex) basics.BalanceRecord { + var br basics.BalanceRecord + br.Addr = addr + + br.MicroAlgos = basics.MicroAlgos{Raw: 500000000} + br.Status = basics.Status(1) + br.AssetParams = map[basics.AssetIndex]basics.AssetParams{ + assetIdx: { + Total: 100, + UnitName: "tok", + AssetName: "asset", + }, + } + br.Assets = map[basics.AssetIndex]basics.AssetHolding{ + assetIdx: { + Amount: 10, + }, + } + br.AppLocalStates = map[basics.AppIndex]basics.AppLocalState{ + appIdx: { + Schema: basics.StateSchema{ + NumUint: 2, + NumByteSlice: 3, + }, + KeyValue: basics.TealKeyValue{ + "lkeyint": { + Type: basics.TealType(basics.TealUintType), + Uint: 1, + }, + "lkeybyte": { + Type: basics.TealType(basics.TealBytesType), + Bytes: "local", + }, + }, + }, + } + br.AppParams = map[basics.AppIndex]basics.AppParams{ + appIdx: { + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1, 1}, + StateSchemas: basics.StateSchemas{ + LocalStateSchema: basics.StateSchema{ + NumUint: 2, + NumByteSlice: 3, + }, + GlobalStateSchema: basics.StateSchema{ + NumUint: 1, + NumByteSlice: 2, + }, + }, + GlobalState: basics.TealKeyValue{ + "gkeyint": { + Type: basics.TealType(basics.TealUintType), + Uint: 2, + }, + "gkeybyte": { + Type: basics.TealType(basics.TealBytesType), + Bytes: "global", + }, + }, + }, + } + return br +} + +func makeSampleSerializedBalanceRecord(addr basics.Address, toJSON bool) []byte { + br := makeSampleBalanceRecord(addr, 50, 100) + if toJSON { + return protocol.EncodeJSON(&br) + } + return protocol.EncodeMsgp(&br) +} + +func TestBalanceJSONInput(t *testing.T) { + a := require.New(t) + + addr, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + + dp := DebugParams{ + BalanceBlob: []byte(balanceSample), + } + balances, err := balanceRecordsFromParams(&dp) + a.NoError(err) + a.Equal(1, len(balances)) + a.Equal(addr, balances[0].Addr) + + dp.BalanceBlob = []byte("[" + strings.Join([]string{balanceSample, balanceSample}, ",") + "]") + balances, err = balanceRecordsFromParams(&dp) + a.NoError(err) + a.Equal(2, len(balances)) + a.Equal(addr, balances[0].Addr) + a.Equal(basics.MicroAlgos{Raw: 500000000}, balances[1].MicroAlgos) +} + +func TestBalanceMessagePackInput(t *testing.T) { + a := require.New(t) + addr, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + + var br basics.BalanceRecord + err = protocol.DecodeJSON([]byte(balanceSample), &br) + a.NoError(err) + + blob := protocol.EncodeMsgp(&br) + dp := DebugParams{ + BalanceBlob: blob, + } + + balances, err := balanceRecordsFromParams(&dp) + a.NoError(err) + a.Equal(1, len(balances)) + a.Equal(addr, balances[0].Addr) + + dp.BalanceBlob = append(blob, blob...) + balances, err = balanceRecordsFromParams(&dp) + a.NoError(err) + a.Equal(2, len(balances)) + a.Equal(addr, balances[0].Addr) + a.Equal(basics.MicroAlgos{Raw: 500000000}, balances[1].MicroAlgos) +} + +func TestDebugEnvironment(t *testing.T) { + a := require.New(t) + + sender, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + + receiver, err := basics.UnmarshalChecksumAddress("PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI") + a.NoError(err) + + addr1, err := basics.UnmarshalChecksumAddress("OC6IROKUJ7YCU5NV76AZJEDKYQG33V2CJ7HAPVQ4ENTAGMLIOINSQ6EKGE") + a.NoError(err) + + addr2, err := basics.UnmarshalChecksumAddress("YYKRMERAFXMXCDWMBNR6BUUWQXDCUR53FPUGXLUYS7VNASRTJW2ENQ7BMQ") + a.NoError(err) + + // make balance records + assetIdx := basics.AssetIndex(50) + appIdx := basics.AppIndex(100) + appIdx1 := basics.AppIndex(200) + appIdx2 := basics.AppIndex(300) + brs := makeSampleBalanceRecord(sender, assetIdx, appIdx) + brr := makeSampleBalanceRecord(receiver, assetIdx, appIdx) + bra1 := makeSampleBalanceRecord(addr1, assetIdx, appIdx1) + bra2 := makeSampleBalanceRecord(addr2, assetIdx, appIdx2) + // fix receiver so that it only has asset holding and app local + delete(brr.AssetParams, assetIdx) + delete(brr.AppParams, appIdx) + delete(bra1.AssetParams, assetIdx) + delete(bra2.AssetParams, assetIdx) + balanceBlob := protocol.EncodeMsgp(&brs) + balanceBlob = append(balanceBlob, protocol.EncodeMsgp(&brr)...) + balanceBlob = append(balanceBlob, protocol.EncodeMsgp(&bra1)...) + balanceBlob = append(balanceBlob, protocol.EncodeMsgp(&bra2)...) + + // make transaction group: app call + sample payment + txn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Header: transactions.Header{ + Sender: sender, + Fee: basics.MicroAlgos{Raw: 100}, + Note: []byte{1, 2, 3}, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + ApplicationArgs: [][]byte{[]byte("ALGO"), []byte("RAND")}, + Accounts: []basics.Address{receiver}, + ForeignApps: []basics.AppIndex{appIdx1}, + }, + }, + } + + txnEnc := protocol.EncodeJSON(&txn) + txnBlob := []byte("[" + strings.Join([]string{string(txnEnc), txnSample}, ",") + "]") + + // create sample programs that checks all the environment: + // transaction fields, global properties, + source := `global Round +int 222 +== +global LatestTimestamp +int 333 +== +&& +global GroupSize +int 2 +== +&& +global LogicSigVersion +int 2 +>= +&& +txn NumAppArgs +int 2 +== +&& +txn NumAccounts +int 1 +== +&& +txna Accounts 0 +addr 47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU +== +&& +txna Accounts 1 +addr PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI +== +&& +gtxn 1 Amount +int 1000 +== +&& +// now check stateful opcodes +int 0 +balance +int 500000000 +== +&& +int 1 +int 100 +app_opted_in +int 1 +== +&& +int 1 +int 200 +app_opted_in +int 1 +!= +&& +int 1 +byte 0x6c6b6579696e74 // lkeyint +app_local_get +int 1 +== +&& +int 0 +int 100 +byte 0x6c6b657962797465 // lkeybyte +app_local_get_ex +bnz ok +err +ok: +byte 0x6c6f63616c // local +== +&& +byte 0x676b6579696e74 // gkeyint +app_global_get +int 2 +== +&& +int 1 // ForeignApps index +byte 0x676b657962797465 // gkeybyte +app_global_get_ex +bnz ok2 +err +ok2: +byte 0x676c6f62616c // global +== +&& + +// write +int 1 +byte 0x6c6b65796279746565 // lkeybytee +byte 0x6c6f63616c // local +app_local_put +byte 0x676b65796279746565 // gkeybytee +byte 0x676c6f62616c // global +app_global_put +int 1 +byte 0x6c6b65796279746565 // lkeybytee +app_local_del +byte 0x676b65796279746565 +app_global_del + +// asssets +int 1 +int 50 +asset_holding_get AssetBalance +bnz ok3 +err +ok3: +int 10 +== +&& +int 0 +int 50 +asset_params_get AssetTotal +bnz ok4 +err +ok4: +int 100 +== +&& +` + + ds := DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{[]byte(source)}, + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + Proto: "future", + Round: 222, + LatestTimestamp: 333, + GroupIndex: 0, + RunMode: "application", + } + + local := MakeLocalRunner(nil) // no debugger + err = local.Setup(&ds) + a.NoError(err) + + pass, err := local.Run() + a.NoError(err) + a.True(pass) + + // check relaxed - opted in for both + source = `int 1 +int 100 +app_opted_in +int 1 +== +int 1 +int 200 +app_opted_in +int 1 +== +&& +` + ds.Painless = true + ds.ProgramBlobs = [][]byte{[]byte(source)} + err = local.Setup(&ds) + a.NoError(err) + + pass, err = local.Run() + a.NoError(err) + a.True(pass) + ds.Painless = false + + // check ForeignApp + source = ` +int 300 +byte 0x676b657962797465 // gkeybyte +app_global_get_ex +bnz ok +err +ok: +byte 0x676c6f62616c // global +== +` + ds.ProgramBlobs = [][]byte{[]byte(source)} + err = local.Setup(&ds) + a.NoError(err) + + pass, err = local.Run() + a.Error(err) + a.False(pass) +} + +func TestDebugFromPrograms(t *testing.T) { + a := require.New(t) + + txnBlob := []byte("[" + strings.Join([]string{string(txnSample), txnSample}, ",") + "]") + + l := LocalRunner{} + dp := DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{1}}, + TxnBlob: []byte(txnSample), + GroupIndex: 1, + } + + err := l.Setup(&dp) + a.Error(err) + a.Contains(err.Error(), "invalid group index 1 for a single transaction") + + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{1}}, + TxnBlob: txnBlob, + GroupIndex: 3, + } + + err = l.Setup(&dp) + a.Error(err) + a.Contains(err.Error(), "invalid group index 3 for a txn in a transaction group of 2") + + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{1}}, + TxnBlob: txnBlob, + GroupIndex: 0, + AppID: 100, + } + + err = l.Setup(&dp) + a.Error(err) + a.Contains(err.Error(), "unknown run mode") + + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{1}}, + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "signature", + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Nil(l.runs[0].ledger) + + dp = DebugParams{ + ProgramNames: []string{"test", "test"}, + ProgramBlobs: [][]byte{{1}, {1}}, + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "signature", + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(2, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.Equal(0, l.runs[1].groupIndex) + a.NotNil(l.runs[0].eval) + a.Nil(l.runs[0].ledger) + + a.NotNil(l.runs[1].eval) + a.Nil(l.runs[1].ledger) +} + +func TestRunMode(t *testing.T) { + a := require.New(t) + + txnBlob := []byte("[" + strings.Join([]string{string(txnSample), txnSample}, ",") + "]") + l := LocalRunner{} + + // check run mode auto on stateful code + dp := DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{2, 0x20, 1, 1, 0x22, 0x22, 0x61}}, // version, intcb, int 1, int 1, app_opted_in + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "auto", + AppID: 100, + } + + err := l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // check run mode auto on stateless code + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{2, 0x20, 1, 1, 0x22}}, // version, intcb, int 1 + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "auto", + AppID: 100, + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Nil(l.runs[0].ledger) + a.Equal( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // check run mode application + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{2, 0x20, 1, 1, 0x22, 0x22, 0x61}}, // version, intcb, int 1, int 1, app_opted_in + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "application", + AppID: 100, + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // check run mode signature + dp = DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{2, 0x20, 1, 1, 0x22}}, // version, intcb, int 1 + TxnBlob: txnBlob, + GroupIndex: 0, + RunMode: "signature", + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Nil(l.runs[0].ledger) + a.Equal( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) +} + +func TestDebugFromTxn(t *testing.T) { + a := require.New(t) + + sender, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + // make balance records + appIdx := basics.AppIndex(100) + brs := makeSampleBalanceRecord(sender, 0, appIdx+1) + balanceBlob := protocol.EncodeMsgp(&brs) + + // make transaction group: app call + sample payment + appTxn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Header: transactions.Header{ + Sender: sender, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + }, + }, + } + + var payTxn transactions.SignedTxn + err = protocol.DecodeJSON([]byte(txnSample), &payTxn) + a.NoError(err) + + txnBlob := protocol.EncodeMsgp(&appTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + l := LocalRunner{} + dp := DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + } + + err = l.Setup(&dp) + a.Error(err) + a.Contains(err.Error(), "no programs found in transactions") + a.Equal(2, len(l.txnGroup)) + + // ensure clear logic sig program is supposed to be debugged + payTxn.Lsig.Logic = []byte{3} + txnBlob = protocol.EncodeMsgp(&appTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + dp = DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + GroupIndex: 10, // must be ignored + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.runs)) + a.Equal(1, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Equal([]byte{3}, l.runs[0].program) + a.Nil(l.runs[0].ledger) + a.Equal( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // ensure clear approval program is supposed to be debugged + brs = makeSampleBalanceRecord(sender, 0, appIdx) + balanceBlob = protocol.EncodeMsgp(&brs) + + payTxn.Lsig.Logic = nil + appTxn.Txn.Type = protocol.ApplicationCallTx + txnBlob = protocol.EncodeMsgp(&appTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + dp = DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + GroupIndex: 10, // must be ignored + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(2, len(l.txnGroup)) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Equal([]byte{1}, l.runs[0].program) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // ensure clear state program is supposed to be debugged + appTxn.Txn.Type = protocol.ApplicationCallTx + appTxn.Txn.OnCompletion = transactions.ClearStateOC + txnBlob = protocol.EncodeMsgp(&appTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + dp = DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + GroupIndex: 10, // must be ignored + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(2, len(l.txnGroup)) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Equal([]byte{1, 1}, l.runs[0].program) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // check app create txn uses approval program from the txn + appTxn.Txn.Type = protocol.ApplicationCallTx + appTxn.Txn.OnCompletion = transactions.NoOpOC + appTxn.Txn.ApplicationID = 0 + appTxn.Txn.ApprovalProgram = []byte{4} + txnBlob = protocol.EncodeMsgp(&appTxn) + + dp = DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + GroupIndex: 10, // must be ignored + AppID: 100, + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(1, len(l.txnGroup)) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Equal([]byte{4}, l.runs[0].program) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + + // check error on no programs + txnBlob = protocol.EncodeMsgp(&payTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + dp = DebugParams{ + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + GroupIndex: 10, // must be ignored + } + + err = l.Setup(&dp) + a.Error(err) + a.Equal(2, len(l.txnGroup)) +} + +func TestLocalLedger(t *testing.T) { + a := require.New(t) + + sender, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + // make balance records + appIdx := basics.AppIndex(100) + assetIdx := basics.AssetIndex(50) + brs := makeSampleBalanceRecord(sender, assetIdx, appIdx) + balanceBlob := protocol.EncodeMsgp(&brs) + + // make transaction group: app call + sample payment + appTxn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Header: transactions.Header{ + Sender: sender, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + }, + }, + } + + var payTxn transactions.SignedTxn + err = protocol.DecodeJSON([]byte(txnSample), &payTxn) + a.NoError(err) + + txnBlob := protocol.EncodeMsgp(&appTxn) + txnBlob = append(txnBlob, protocol.EncodeMsgp(&payTxn)...) + + l := LocalRunner{} + dp := DebugParams{ + ProgramNames: []string{"test"}, + ProgramBlobs: [][]byte{{1}}, + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + RunMode: "application", + GroupIndex: 0, + Round: 100, + LatestTimestamp: 333, + } + + err = l.Setup(&dp) + a.NoError(err) + a.Equal(2, len(l.txnGroup)) + a.Equal(1, len(l.runs)) + a.Equal(0, l.runs[0].groupIndex) + a.NotNil(l.runs[0].eval) + a.Equal([]byte{1}, l.runs[0].program) + a.NotNil(l.runs[0].ledger) + a.NotEqual( + reflect.ValueOf(logic.Eval).Pointer(), + reflect.ValueOf(l.runs[0].eval).Pointer(), + ) + ledger := l.runs[0].ledger + a.Equal(basics.Round(100), ledger.Round()) + a.Equal(int64(333), ledger.LatestTimestamp()) + + balance, err := ledger.Balance(sender) + a.NoError(err) + a.Equal(basics.MicroAlgos{Raw: 500000000}, balance) + + holdings, err := ledger.AssetHolding(sender, assetIdx) + a.NoError(err) + a.Equal(basics.AssetHolding{Amount: 10, Frozen: false}, holdings) + holdings, err = ledger.AssetHolding(sender, assetIdx+1) + a.Error(err) + + params, err := ledger.AssetParams(sender, assetIdx) + a.NoError(err) + a.Equal(uint64(100), params.Total) + a.Equal("tok", params.UnitName) + params, err = ledger.AssetParams(payTxn.Txn.Receiver, assetIdx) + a.Error(err) + + tkv, err := ledger.AppGlobalState(0) + a.NoError(err) + a.Equal(uint64(2), tkv["gkeyint"].Uint) + tkv, err = ledger.AppGlobalState(appIdx) + a.NoError(err) + a.Equal("global", tkv["gkeybyte"].Bytes) + tkv, err = ledger.AppGlobalState(appIdx + 1) + a.Error(err) + + tkv, err = ledger.AppLocalState(sender, 0) + a.NoError(err) + a.Equal(uint64(1), tkv["lkeyint"].Uint) + tkv, err = ledger.AppLocalState(sender, appIdx) + a.NoError(err) + a.Equal("local", tkv["lkeybyte"].Bytes) + tkv, err = ledger.AppLocalState(sender, appIdx+1) + a.Error(err) + tkv, err = ledger.AppLocalState(payTxn.Txn.Receiver, appIdx) + a.Error(err) +} diff --git a/cmd/tealdbg/main.go b/cmd/tealdbg/main.go new file mode 100644 index 0000000000..f06f5978a5 --- /dev/null +++ b/cmd/tealdbg/main.go @@ -0,0 +1,265 @@ +// 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 . + +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" +) + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +var rootCmd = &cobra.Command{ + Use: "tealdbg", + Short: "Algorand TEAL Debugger", + Long: `Debug a local or remote TEAL code in controlled environment +with Web or Chrome DevTools frontends`, + Run: func(cmd *cobra.Command, args []string) { + //If no arguments passed, we should fallback to help + cmd.HelpFunc()(cmd, args) + }, +} + +var debugCmd = &cobra.Command{ + Use: "debug program.tok [program.teal ...]", + Short: "Debug a local TEAL program(s)", + Long: `Debug a local TEAL program(s) in controlled environment`, + Run: func(cmd *cobra.Command, args []string) { + debugLocal(args) + // //If no arguments passed, we should fallback to help + // cmd.HelpFunc()(cmd, args) + }, +} + +var remoteCmd = &cobra.Command{ + Use: "remote", + Short: "Debug remote TEAL program", + Long: `Start the server and wait upcoming debug connections from remote TEAL evaluator`, + Run: func(cmd *cobra.Command, args []string) { + debugRemote() + }, +} + +// cobraStringValue is a cobra's string flag with restricted values +type cobraStringValue struct { + value string + allowed []string + isSet bool +} + +func makeCobraStringValue(value string, others []string) *cobraStringValue { + c := new(cobraStringValue) + c.value = value + c.allowed = make([]string, 0, len(others)+1) + c.allowed = append(c.allowed, value) + for _, s := range others { + c.allowed = append(c.allowed, s) + } + return c +} + +func (c *cobraStringValue) String() string { return c.value } +func (c *cobraStringValue) Type() string { return "string" } +func (c *cobraStringValue) IsSet() bool { return c.isSet } + +func (c *cobraStringValue) Set(other string) error { + for _, s := range c.allowed { + if other == s { + c.value = other + c.isSet = true + return nil + } + } + return fmt.Errorf("value %s not allowed", other) +} + +func (c *cobraStringValue) AllowedString() string { + return strings.Join(c.allowed, ", ") +} + +type frontendValue struct { + *cobraStringValue +} + +func (f *frontendValue) Make(router *mux.Router, appAddress string) (da DebugAdapter) { + switch f.value { + case "web": + wa := MakeWebPageAdapter(&WebPageAdapterParams{router, appAddress}) + return wa + case "cdt": + fallthrough + default: + cdt := MakeCDTAdapter(&CDTAdapterParams{router, appAddress, verbose}) + return cdt + } +} + +type runModeValue struct { + *cobraStringValue +} + +var frontend frontendValue = frontendValue{makeCobraStringValue("cdt", []string{"web"})} +var proto string +var txnFile string +var groupIndex int +var balanceFile string +var ddrFile string +var roundNumber int +var timestamp int64 +var runMode runModeValue = runModeValue{makeCobraStringValue("auto", []string{"signature", "application"})} +var port int +var noFirstRun bool +var noBrowserCheck bool +var noSourceMap bool +var verbose bool +var painless bool +var appID int + +func init() { + rootCmd.PersistentFlags().VarP(&frontend, "frontend", "f", "Frontend to use: "+frontend.AllowedString()) + rootCmd.PersistentFlags().IntVar(&port, "remote-debugging-port", 9392, "Port to listen on") + rootCmd.PersistentFlags().BoolVar(&noFirstRun, "no-first-run", false, "") + rootCmd.PersistentFlags().MarkHidden("no-first-run") + rootCmd.PersistentFlags().BoolVar(&noBrowserCheck, "no-default-browser-check", false, "") + rootCmd.PersistentFlags().MarkHidden("no-default-browser-check") + rootCmd.PersistentFlags().BoolVar(&noSourceMap, "no-source-map", false, "Do not generate source maps") + rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output") + + debugCmd.Flags().StringVarP(&proto, "proto", "p", "", "Consensus protocol version for TEAL") + debugCmd.Flags().StringVarP(&txnFile, "txn", "t", "", "Transaction(s) to evaluate TEAL on in form of json or msgpack file") + debugCmd.Flags().IntVarP(&groupIndex, "group-index", "g", 0, "Transaction index in a txn group") + debugCmd.Flags().StringVarP(&balanceFile, "balance", "b", "", "Balance records to evaluate stateful TEAL on in form of json or msgpack file") + debugCmd.Flags().StringVarP(&ddrFile, "dryrun-req", "d", "", "Program(s) and state(s) in dryrun REST request format") + debugCmd.Flags().IntVarP(&appID, "app-id", "a", 1380011588, "Application ID for stateful TEAL if not set in transaction(s)") + debugCmd.Flags().IntVarP(&roundNumber, "round", "r", 1095518031, "Ledger round number to evaluate stateful TEAL on") + debugCmd.Flags().Int64VarP(×tamp, "latest-timestamp", "l", 0, "Latest confirmed timestamp to evaluate stateful TEAL on") + debugCmd.Flags().VarP(&runMode, "mode", "m", "TEAL evaluation mode: "+runMode.AllowedString()) + debugCmd.Flags().BoolVar(&painless, "painless", false, "Automatically create balance record for all accounts and applications") + + rootCmd.AddCommand(debugCmd) + rootCmd.AddCommand(remoteCmd) +} + +func debugRemote() { + ds := makeDebugServer(port, &frontend, nil) + + ds.startRemote() +} + +func debugLocal(args []string) { + // simple pre-invalidation + if roundNumber < 0 { + log.Fatalln("Invalid round") + } + + // program can be set either directly + // or with SignedTxn.Lsig.Logic, + // or with BalanceRecord.AppParams.ApprovalProgram + if len(args) == 0 && (len(txnFile) == 0 || len(balanceFile) == 0) && len(ddrFile) == 0 { + log.Fatalln("No program to debug: must specify program(s), or transaction(s) and a balance record(s), or dryrun-req object") + } + + if len(args) == 0 && groupIndex != 0 { + log.Fatalln("Error: group-index may be only set only along with program(s)") + } + + if len(args) == 0 && runMode.IsSet() { + log.Fatalln("Error: mode may be only set only along with program(s)") + } + + if len(txnFile) != 0 && len(ddrFile) != 0 { + log.Fatalln("Error: cannot specify both transaction(s) and dryrun-req") + } + + if len(balanceFile) != 0 && len(ddrFile) != 0 { + log.Fatalln("Error: cannot specify both balance records(s) and dryrun-req") + } + + var programNames []string + var programBlobs [][]byte + if len(args) > 0 { + programNames = make([]string, len(args)) + programBlobs = make([][]byte, len(args)) + for i, file := range args { + data, err := ioutil.ReadFile(file) + if err != nil { + log.Fatalf("Error program reading %s: %s", file, err) + } + programNames[i] = file + programBlobs[i] = data + } + } + + var err error + var txnBlob []byte + if len(txnFile) > 0 { + txnBlob, err = ioutil.ReadFile(txnFile) + if err != nil { + log.Fatalf("Error txn reading %s: %s", balanceFile, err) + } + } + + var balanceBlob []byte + if len(balanceFile) > 0 { + balanceBlob, err = ioutil.ReadFile(balanceFile) + if err != nil { + log.Fatalf("Error balance reading %s: %s", balanceFile, err) + } + } + + var ddrBlob []byte + if len(ddrFile) > 0 { + ddrBlob, err = ioutil.ReadFile(ddrFile) + if err != nil { + log.Fatalf("Error dryrun-dump reading %s: %s", ddrFile, err) + } + } + + dp := DebugParams{ + ProgramNames: programNames, + ProgramBlobs: programBlobs, + Proto: proto, + TxnBlob: txnBlob, + GroupIndex: groupIndex, + BalanceBlob: balanceBlob, + DdrBlob: ddrBlob, + Round: roundNumber, + LatestTimestamp: timestamp, + RunMode: runMode.String(), + DisableSourceMap: noSourceMap, + AppID: appID, + Painless: painless, + } + + ds := makeDebugServer(port, &frontend, &dp) + + err = ds.startDebug() + if err != nil { + log.Fatalf("Debug error: %s", err.Error()) + } +} diff --git a/cmd/tealdbg/remote.go b/cmd/tealdbg/remote.go new file mode 100644 index 0000000000..04bead366f --- /dev/null +++ b/cmd/tealdbg/remote.go @@ -0,0 +1,108 @@ +// 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 . + +package main + +import ( + "io" + "net/http" + + "github.com/gorilla/mux" + + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/protocol" +) + +// RemoteHookAdapter provides HTTP transport for WebDebuggerHook +type RemoteHookAdapter struct { + debugger *Debugger +} + +// MakeRemoteHook creates new RemoteHookAdapter +func MakeRemoteHook(debugger *Debugger) *RemoteHookAdapter { + r := new(RemoteHookAdapter) + r.debugger = debugger + return r +} + +// Setup adds HTTP handlers for remote WebDebuggerHook +func (rha *RemoteHookAdapter) Setup(router *mux.Router) { + router.HandleFunc("/exec/register", rha.registerHandler).Methods("POST") + router.HandleFunc("/exec/update", rha.updateHandler).Methods("POST") + router.HandleFunc("/exec/complete", rha.completeHandler).Methods("POST") +} + +func (rha *RemoteHookAdapter) decodeState(body io.Reader) (state logic.DebugState, err error) { + dec := protocol.NewJSONDecoder(body) + err = dec.Decode(&state) + return +} + +func (rha *RemoteHookAdapter) registerHandler(w http.ResponseWriter, r *http.Request) { + state, err := rha.decodeState(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Register, and wait for user to acknowledge registration + err = rha.debugger.Register(&state) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Proceed! + w.WriteHeader(http.StatusOK) + return +} + +func (rha *RemoteHookAdapter) updateHandler(w http.ResponseWriter, r *http.Request) { + state, err := rha.decodeState(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Ask debugger to process and wait to continue + err = rha.debugger.Update(&state) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) + return +} + +func (rha *RemoteHookAdapter) completeHandler(w http.ResponseWriter, r *http.Request) { + state, err := rha.decodeState(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Ask debugger to process and wait to continue + err = rha.debugger.Complete(&state) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Proceed! + w.WriteHeader(http.StatusOK) + return +} diff --git a/cmd/tealdbg/samples/balances.json b/cmd/tealdbg/samples/balances.json new file mode 100644 index 0000000000..82352c73ac --- /dev/null +++ b/cmd/tealdbg/samples/balances.json @@ -0,0 +1,84 @@ +[ + { + "addr": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "onl": 1, + "algo": 500000000, + "apar": { + "50": { + "an": "asset", + "t": 100, + "un": "tok" + } + }, + "asset": { + "50": { + "a": 10 + } + }, + "appl": { + "100": { + "hsch": { + "nbs": 3, + "nui": 2 + }, + "tkv": { + "lkeybyte": { + "tb": "local", + "tt": 1 + }, + "lkeyint": { + "tt": 2, + "ui": 1 + } + } + } + }, + "appp": { + "100": { + "approv": "AQE=", + "gs": { + "gkeyint": { + "tt": 2, + "ui": 2 + } + }, + "gsch": { + "nbs": 1, + "nui": 1 + }, + "lsch": { + "nbs": 3, + "nui": 2 + } + } + } + }, + { + "addr": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "onl": 1, + "algo": 500000000, + "asset": { + "50": { + "a": 10 + } + }, + "appl": { + "100": { + "hsch": { + "nbs": 3, + "nui": 2 + }, + "tkv": { + "lkeybyte": { + "tb": "local", + "tt": 1 + }, + "lkeyint": { + "tt": 2, + "ui": 1 + } + } + } + } + } +] \ No newline at end of file diff --git a/cmd/tealdbg/samples/branch.teal b/cmd/tealdbg/samples/branch.teal new file mode 100644 index 0000000000..76905ecb9a --- /dev/null +++ b/cmd/tealdbg/samples/branch.teal @@ -0,0 +1,13 @@ +// add two integers +int 1 +int 2 ++ +// must be equal 3 +int 3 +== +bnz ok +// error if not equal +err +ok: +// return 1 +int 1 diff --git a/cmd/tealdbg/samples/calls_count.teal b/cmd/tealdbg/samples/calls_count.teal new file mode 100644 index 0000000000..4df5cdee2d --- /dev/null +++ b/cmd/tealdbg/samples/calls_count.teal @@ -0,0 +1,22 @@ +#pragma version 2 +// a simple global and local calls counter app +byte "counter" +dup +app_global_get +int 1 ++ +app_global_put // update the counter +int 0 +int 0 +app_opted_in +bnz opted_in +err +opted_in: +int 0 +byte "counter" +dup2 +app_local_get +int 1 // increment ++ +app_local_put +int 1 diff --git a/cmd/tealdbg/samples/calls_count_balance.json b/cmd/tealdbg/samples/calls_count_balance.json new file mode 100644 index 0000000000..c790a625b1 --- /dev/null +++ b/cmd/tealdbg/samples/calls_count_balance.json @@ -0,0 +1,25 @@ +{ + "addr": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "onl": 1, + "algo": 500000000, + "appl": { + "100": { + "hsch": { + "nbs": 3, + "nui": 2 + } + } + }, + "appp": { + "100": { + "gsch": { + "nbs": 1, + "nui": 1 + }, + "lsch": { + "nbs": 3, + "nui": 2 + } + } + } +} diff --git a/cmd/tealdbg/samples/calls_count_txn.json b/cmd/tealdbg/samples/calls_count_txn.json new file mode 100644 index 0000000000..715454ff9e --- /dev/null +++ b/cmd/tealdbg/samples/calls_count_txn.json @@ -0,0 +1,12 @@ +{ + "sig": "", + "txn": { + "apid": 100, + "fee": 1176, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "note": "6gAVR0Nsv5Y=", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "appl" + } +} \ No newline at end of file diff --git a/cmd/tealdbg/samples/txn.json b/cmd/tealdbg/samples/txn.json new file mode 100644 index 0000000000..07122b5a3d --- /dev/null +++ b/cmd/tealdbg/samples/txn.json @@ -0,0 +1,17 @@ +{ + "sig": "+FQBnfGQMNxzwW85WjpSKfOYoEKqzTChhJ+h2WYEx9C8Zt5THdKvHLd3IkPO/usubboFG/0Wcvb8C5Ps1h+IBQ==", + "txn": { + "amt": 1000, + "close": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", + "fee": 1176, + "fv": 12466, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "lv": 13466, + "note": "6gAVR0Nsv5Y=", + "rcv": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "pay" + } +} + diff --git a/cmd/tealdbg/samples/txn.msgp b/cmd/tealdbg/samples/txn.msgp new file mode 100644 index 0000000000000000000000000000000000000000..285defc71a82234553904fcfb184588978c7b34c GIT binary patch literal 296 zcmV+@0oVS5qjPCz#6b8|0iE%XFx+#&Z#h~zQYrJ8phBw6FrkE>q1k2x$I!fH-cudY zuN=2`B16vp>n?4&1snYqa`yZSlkCRV-|=%hj){8yu9Wo69-n4)HO%`mc~XJu}zWMy`4Wppie zGcztQqGxEtASMeS380gxhKqSvaxbOjM(yYA7v$JnOn9%bMJAB#(xPm3%`}>%Zf|sD z#0csD6-PsCzm}tNV|K(Kdu-xQ>szeAv1H<$A}m++_C3Rg)Hwu#SV0_L;SYQQqjPR# u#31MJ_)P|Z9r^Pdjfaa7RrF^aQK5yx0FZ`f^hsE7XrZKZd2nT;aAA4=A(Alw literal 0 HcmV?d00001 diff --git a/cmd/tealdbg/samples/txn_group.json b/cmd/tealdbg/samples/txn_group.json new file mode 100644 index 0000000000..d3f0796874 --- /dev/null +++ b/cmd/tealdbg/samples/txn_group.json @@ -0,0 +1,34 @@ +[ +{ + "sig": "+FQBnfGQMNxzwW85WjpSKfOYoEKqzTChhJ+h2WYEx9C8Zt5THdKvHLd3IkPO/usubboFG/0Wcvb8C5Ps1h+IBQ==", + "txn": { + "amt": 1000, + "close": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", + "fee": 1176, + "fv": 12466, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "lv": 13466, + "note": "6gAVR0Nsv5Y=", + "rcv": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "pay" + } +}, +{ + "sig": "+FQBnfGQMNxzwW85WjpSKfOYoEKqzTChhJ+h2WYEx9C8Zt5THdKvHLd3IkPO/usubboFG/0Wcvb8C5Ps1h+IBQ==", + "txn": { + "amt": 1000, + "close": "IDUTJEUIEVSMXTU4LGTJWZ2UE2E6TIODUKU6UW3FU3UKIQQ77RLUBBBFLA", + "fee": 1176, + "fv": 12466, + "gen": "devnet-v33.0", + "gh": "JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=", + "lv": 13466, + "note": "6gAVR0Nsv5Y=", + "rcv": "PNWOET7LLOWMBMLE4KOCELCX6X3D3Q4H2Q4QJASYIEOF7YIPPQBG3YQ5YI", + "snd": "47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU", + "type": "pay" + } +} +] diff --git a/cmd/tealdbg/samples/txn_group.msgp b/cmd/tealdbg/samples/txn_group.msgp new file mode 100644 index 0000000000000000000000000000000000000000..5b3330b231e7c1f569b091318d04c7093550fa82 GIT binary patch literal 592 zcmZo#T%4JH#NkH>Azw{-~Mnup zHRbfVIg!g|r-!I@zFc^C(aKlRsmoq0agzTNzBny4^(@PbMQLSc4K^)KPt9ACl3JFR zTB2KKY^-OnC_Uqdf*Q91=Yq*A+PW(viu9L0b$k2#t@x80F+LURH@K=zczbD4PT5(L zSxfTrOHz+;ykZb_ch1>AZE;a@*%5{6oJanzqt_hRnDS_jl1}*7Z?=cquUN7)ML5dD zKjg1rT3no$azx?zhabLdjj}&K%k;K)3x6H(y=01d|^Z~y=R literal 0 HcmV?d00001 diff --git a/cmd/tealdbg/server.go b/cmd/tealdbg/server.go new file mode 100644 index 0000000000..cc2049eb18 --- /dev/null +++ b/cmd/tealdbg/server.go @@ -0,0 +1,130 @@ +// 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 . + +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/algorand/websocket" + "github.com/gorilla/mux" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 20480, + WriteBufferSize: 20480, + CheckOrigin: func(r *http.Request) bool { + if len(r.Header.Get("Origin")) == 0 { + return true + } + if strings.HasPrefix(r.Header.Get("Origin"), "devtools://") { + return true + } + if strings.HasPrefix(r.Header.Get("Origin"), "http://localhost") { + return true + } + return false + }, +} + +// DebugServer is Debugger + HTTP/WS handlers for frontends +type DebugServer struct { + debugger *Debugger + frontend DebugAdapter + router *mux.Router + server *http.Server + remote *RemoteHookAdapter + params *DebugParams +} + +// DebugParams is a container for debug parameters +type DebugParams struct { + ProgramNames []string + ProgramBlobs [][]byte + Proto string + TxnBlob []byte + GroupIndex int + BalanceBlob []byte + DdrBlob []byte + Round int + LatestTimestamp int64 + RunMode string + DisableSourceMap bool + AppID int + Painless bool +} + +// FrontendFactory interface for attaching debug frontends +type FrontendFactory interface { + Make(router *mux.Router, appAddress string) (da DebugAdapter) +} + +func makeDebugServer(port int, ff FrontendFactory, dp *DebugParams) DebugServer { + debugger := MakeDebugger() + + router := mux.NewRouter() + appAddress := fmt.Sprintf("localhost:%d", port) + + da := ff.Make(router, appAddress) + debugger.AddAdapter(da) + + server := &http.Server{ + Handler: router, + Addr: appAddress, + WriteTimeout: time.Duration(0), + ReadTimeout: time.Duration(0), + } + + return DebugServer{ + debugger: debugger, + frontend: da, + router: router, + server: server, + params: dp, + } +} + +func (ds *DebugServer) startRemote() { + remote := MakeRemoteHook(ds.debugger) + remote.Setup(ds.router) + ds.remote = remote + + log.Printf("starting server on %s", ds.server.Addr) + ds.server.ListenAndServe() +} + +func (ds *DebugServer) startDebug() (err error) { + local := MakeLocalRunner(ds.debugger) + if err = local.Setup(ds.params); err != nil { + return + } + + go ds.server.ListenAndServe() + defer ds.server.Shutdown(context.Background()) + + err = local.RunAll() + if err != nil { + return + } + + ds.frontend.WaitForCompletion() + return +} diff --git a/cmd/tealdbg/util.go b/cmd/tealdbg/util.go new file mode 100644 index 0000000000..c7afc6ceb1 --- /dev/null +++ b/cmd/tealdbg/util.go @@ -0,0 +1,128 @@ +// 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 . + +package main + +import ( + "bytes" + "strconv" + "sync/atomic" +) + +type atomicString struct { + value atomic.Value +} + +func (s *atomicString) Store(other string) { + s.value.Store(other) +} + +func (s *atomicString) Load() string { + result := s.value.Load() + if result != nil { + if value, ok := result.(string); ok { + return value + } + } + return "" +} + +func (s *atomicString) Length() int { + return len(s.Load()) +} + +type atomicBool struct { + value uint32 +} + +func (b *atomicBool) SetTo(other bool) { + var converted uint32 = 0 + if other { + converted = 1 + } + atomic.StoreUint32(&b.value, converted) +} + +func (b *atomicBool) IsSet() bool { + return atomic.LoadUint32(&b.value) != 0 +} + +type atomicInt struct { + value int32 +} + +func (i *atomicInt) Store(other int) { + atomic.StoreInt32(&i.value, int32(other)) +} + +func (i *atomicInt) Load() int { + return int(atomic.LoadInt32(&i.value)) +} + +func (i *atomicInt) Add(other int) int { + return int(atomic.AddInt32(&i.value, int32(other))) +} + +// IsText checks if the input has all printable characters with strconv.IsPrint +func IsText(data []byte) bool { + printable := true + for i := 0; i < len(data); i++ { + if !strconv.IsPrint(rune(data[i])) { + printable = false + break + } + } + return printable +} + +// IsTextFile checks the input with strconv.IsPrint and for tabs and new lines +func IsTextFile(data []byte) bool { + printable := true + for i := 0; i < len(data); i++ { + ch := data[i] + if !strconv.IsPrint(rune(ch)) && ch != '\n' && ch != '\r' && ch != '\t' { + printable = false + break + } + } + return printable +} + +const b64table string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +// IntToVLQ writes out value to bytes.Buffer +func IntToVLQ(v int, buf *bytes.Buffer) { + v <<= 1 + if v < 0 { + v = -v + v |= 1 + } + for v >= 32 { + buf.WriteByte(b64table[32|(v&31)]) + v >>= 5 + } + buf.WriteByte(b64table[v]) +} + +// MakeSourceMapLine creates source map mapping's line entry +func MakeSourceMapLine(tcol, sindex, sline, scol int) string { + buf := bytes.NewBuffer(nil) + IntToVLQ(tcol, buf) + IntToVLQ(sindex, buf) + IntToVLQ(sline, buf) + IntToVLQ(scol, buf) + return buf.String() +} diff --git a/cmd/tealdbg/util_test.go b/cmd/tealdbg/util_test.go new file mode 100644 index 0000000000..0e6bd28be9 --- /dev/null +++ b/cmd/tealdbg/util_test.go @@ -0,0 +1,33 @@ +// 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 . + +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVLQ(t *testing.T) { + a := require.New(t) + + a.Equal("AAAA", MakeSourceMapLine(0, 0, 0, 0)) + a.Equal("AACA", MakeSourceMapLine(0, 0, 1, 0)) + a.Equal("AAEA", MakeSourceMapLine(0, 0, 2, 0)) + a.Equal("AAgBA", MakeSourceMapLine(0, 0, 16, 0)) + a.Equal("AAggBA", MakeSourceMapLine(0, 0, 512, 0)) +} diff --git a/cmd/tealdbg/webdbg.go b/cmd/tealdbg/webdbg.go new file mode 100644 index 0000000000..5c0608013b --- /dev/null +++ b/cmd/tealdbg/webdbg.go @@ -0,0 +1,250 @@ +// 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 . + +package main + +//go:generate ./bundle_home_html.sh + +import ( + "bytes" + "encoding/json" + "html/template" + "log" + "net/http" + + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-deadlock" + "github.com/algorand/websocket" + "github.com/gorilla/mux" +) + +// WebPageAdapter is web page debugger +type WebPageAdapter struct { + mu deadlock.Mutex + sessions map[string]wpaSession + apiAddress string + done chan struct{} +} + +type wpaSession struct { + debugger Control + notifications chan Notification +} + +// ExecID is a unique execution ID +type ExecID string + +// ConfigRequest tells us what breakpoints to hit, if any +type ConfigRequest struct { + debugConfig + ExecID ExecID `json:"execid"` +} + +// ContinueRequest tells a particular execution to continue +type ContinueRequest struct { + ExecID ExecID `json:"execid"` +} + +// WebPageAdapterParams initialization parameters +type WebPageAdapterParams struct { + router *mux.Router + apiAddress string +} + +// MakeWebPageAdapter creates new WebPageAdapter +func MakeWebPageAdapter(ctx interface{}) (a *WebPageAdapter) { + params, ok := ctx.(*WebPageAdapterParams) + if !ok { + panic("MakeWebPageAdapter expected CDTAdapterParams") + } + + a = new(WebPageAdapter) + a.sessions = make(map[string]wpaSession) + a.apiAddress = params.apiAddress + a.done = make(chan struct{}) + + params.router.HandleFunc("/", a.homeHandler).Methods("GET") + params.router.HandleFunc("/exec/step", a.stepHandler).Methods("POST") + params.router.HandleFunc("/exec/config", a.configHandler).Methods("POST") + params.router.HandleFunc("/exec/continue", a.continueHandler).Methods("POST") + + params.router.HandleFunc("/ws", a.subscribeHandler) + + return a +} + +// SessionStarted registers new session +func (a *WebPageAdapter) SessionStarted(sid string, debugger Control, ch chan Notification) { + a.mu.Lock() + defer a.mu.Unlock() + + a.sessions[sid] = wpaSession{debugger, ch} + + log.Printf("Open %s in a web browser", a.apiAddress) +} + +// SessionEnded removes the session +func (a *WebPageAdapter) SessionEnded(sid string) { + a.mu.Lock() + defer a.mu.Unlock() + + delete(a.sessions, sid) +} + +// WaitForCompletion waits session to complete +func (a *WebPageAdapter) WaitForCompletion() { + <-a.done +} + +func (a *WebPageAdapter) homeHandler(w http.ResponseWriter, r *http.Request) { + home, err := template.New("home").Parse(homepage) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + home.Execute(w, nil) + return +} + +func (a *WebPageAdapter) stepHandler(w http.ResponseWriter, r *http.Request) { + // Decode a ConfigRequest + var req ConfigRequest + dec := json.NewDecoder(r.Body) + err := dec.Decode(&req) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + // Ensure that we are trying to configure an execution we know about + a.mu.Lock() + s, ok := a.sessions[string(req.ExecID)] + a.mu.Unlock() + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + s.debugger.Step() + + w.WriteHeader(http.StatusOK) + return +} + +func (a *WebPageAdapter) configHandler(w http.ResponseWriter, r *http.Request) { + // Decode a ConfigRequest + var req ConfigRequest + dec := json.NewDecoder(r.Body) + err := dec.Decode(&req) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Ensure that we are trying to configure an execution we know about + a.mu.Lock() + s, ok := a.sessions[string(req.ExecID)] + a.mu.Unlock() + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + // Extract PC from config + breakLine := req.debugConfig.BreakAtLine + if breakLine == -1 { + s.debugger.RemoveBreakpoint(breakLine) + } else { + s.debugger.SetBreakpoint(breakLine) + } + + w.WriteHeader(http.StatusOK) + return +} + +func (a *WebPageAdapter) continueHandler(w http.ResponseWriter, r *http.Request) { + // Decode a ContinueRequest + var req ContinueRequest + dec := json.NewDecoder(r.Body) + err := dec.Decode(&req) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + a.mu.Lock() + s, ok := a.sessions[string(req.ExecID)] + a.mu.Unlock() + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + s.debugger.Resume() + + w.WriteHeader(http.StatusOK) + return +} + +func (a *WebPageAdapter) subscribeHandler(w http.ResponseWriter, r *http.Request) { + defer func() { + close(a.done) + }() + + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("subscribeHandler error: %s\n", err.Error()) + return + } + defer ws.Close() + + // Acknowledge connection + event := Notification{ + Event: "connected", + } + err = ws.WriteJSON(&event) + if err != nil { + return + } + + a.mu.Lock() + // TODO: FIXME: subscribe proto needs be updated and subscribeHandler have to know session ID + // for now take the first session. In most cases there is only one session. + var notifications chan Notification + for _, s := range a.sessions { + notifications = s.notifications + break + } + a.mu.Unlock() + + // Wait on notifications and forward to the user + for { + select { + case notification := <-notifications: + var data bytes.Buffer + enc := protocol.NewJSONEncoder(&data) + err := enc.Encode(notification) + if err != nil { + return + } + err = ws.WriteMessage(websocket.TextMessage, data.Bytes()) + if err != nil { + return + } + } + } +} diff --git a/config/consensus.go b/config/consensus.go index c2eb5fa2ae..e9892d85a4 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -347,7 +347,6 @@ func checkSetAllocBounds(p ConsensusParams) { // executed TEAL instructions should be fine (order of ~1000) checkSetMax(p.MaxAppProgramLen, &MaxStateDeltaKeys) checkSetMax(p.MaxAppProgramLen, &MaxEvalDeltaAccounts) - checkSetMax(p.MaxAppProgramLen, &MaxAppProgramLen) checkSetMax(int(p.LogicSigMaxSize), &MaxLogicSigMaxSize) checkSetMax(p.MaxTxnNoteBytes, &MaxTxnNoteBytes) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index b7baa4ce88..7fc579faea 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -100,7 +100,8 @@ "get": { "description": "Given a specific account public key, this call returns the accounts status, balance and spendable amounts", "produces": [ - "application/json" + "application/json", + "application/msgpack" ], "schemes": [ "http" @@ -115,6 +116,9 @@ "name": "address", "in": "path", "required": true + }, + { + "$ref": "#/parameters/format" } ], "responses": { @@ -150,6 +154,15 @@ "name": "address", "in": "path", "required": true + }, + { + "enum": [ + "json", + "msgpack" + ], + "type": "string", + "name": "format", + "in": "query" } ] }, @@ -366,6 +379,7 @@ ], "responses": { "200": { + "description": "OK", "$ref": "#/responses/PostTransactionsResponse" } } @@ -705,6 +719,132 @@ } ] }, + "/v2/applications/{application-id}": { + "get": { + "description": "Given a application id, it returns application information including creator, approval and clear programs, global and local schemas, and global state.", + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], + "summary": "Get application information.", + "operationId": "GetApplicationByID", + "parameters": [ + { + "type": "integer", + "description": "An application identifier", + "name": "application-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "$ref": "#/responses/ApplicationResponse" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Application Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + }, + "parameters": [ + { + "type": "integer", + "name": "application-id", + "in": "path", + "required": true + } + ] + }, + "/v2/assets/{asset-id}": { + "get": { + "description": "Given a asset id, it returns asset information including creator, name, total supply and special addresses.", + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], + "summary": "Get asset information.", + "operationId": "GetAssetByID", + "parameters": [ + { + "type": "integer", + "description": "An asset identifier", + "name": "asset-id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "$ref": "#/responses/AssetResponse" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Application Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + }, + "parameters": [ + { + "type": "integer", + "name": "asset-id", + "in": "path", + "required": true + } + ] + }, "/v2/teal/compile": { "post": { "description": "Given TEAL source code in plain text, return base64 encoded program bytes and base32 SHA512_256 hash of program bytes (Address style).", @@ -733,7 +873,8 @@ ], "responses": { "200": { - "$ref": "#/responses/PostCompileResponse" + "description": "Successful compilation", + "$ref": "#/responses/CompileResponse" }, "400": { "description": "Bad Request - Teal Compile Error", @@ -780,6 +921,7 @@ ], "responses": { "200": { + "description": "OK", "$ref": "#/responses/CatchpointStartResponse" }, "400": { @@ -858,6 +1000,60 @@ "required": true } ] + }, + "/v2/teal/dryrun": { + "post": { + "description": "Executes TEAL program(s) in context and returns debugging information about the execution.", + "consumes": [ + "application/json", + "application/msgpack" + ], + "produces": [ + "application/json" + ], + "schemes": [ + "http" + ], + "summary": "Provide debugging information for a transaction (or group).", + "operationId": "TealDryrun", + "parameters": [ + { + "description": "Transaction (or group) and any accompanying state-simulation data.", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/DryrunRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "$ref": "#/responses/DryrunResponse" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "401": { + "description": "Invalid API Token", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "default": { + "description": "Unknown Error" + } + } + } } }, "definitions": { @@ -886,6 +1082,17 @@ "description": "specifies the amount of MicroAlgos in the account, without the pending rewards.", "type": "integer" }, + "apps-local-state": { + "description": "\\[appl\\] applications local data stored in this account.\n\nNote the raw object uses `map[int] -\u003e AppLocalState` for this type.", + "type": "array", + "items": { + "$ref": "#/definitions/ApplicationLocalStates" + } + }, + "apps-total-schema": { + "description": "\\[tsch\\] stores the sum of all of the local schemas and global schemas in this account.\n\nNote: the raw account uses `StateSchema` for this type.", + "$ref": "#/definitions/ApplicationStateSchema" + }, "assets": { "description": "\\[asset\\] assets held by this account.\n\nNote the raw object uses `map[int] -\u003e AssetHolding` for this type.", "type": "array", @@ -893,6 +1100,13 @@ "$ref": "#/definitions/AssetHolding" } }, + "created-apps": { + "description": "\\[appp\\] parameters of applications created by this account including app global data.\n\nNote: the raw account uses `map[int] -\u003e AppParams` for this type.", + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, "created-assets": { "description": "\\[apar\\] parameters of assets created by this account.\n\nNote: the raw account uses `map[int] -\u003e Asset` for this type.", "type": "array", @@ -1084,6 +1298,306 @@ } } }, + "ApplicationStateSchema": { + "description": "Specifies maximums on the number of each type that may be stored.", + "type": "object", + "required": [ + "num-uint", + "num-byte-slice" + ], + "properties": { + "num-uint": { + "description": "\\[nui\\] num of uints.", + "type": "integer" + }, + "num-byte-slice": { + "description": "\\[nbs\\] num of byte slices.", + "type": "integer" + } + } + }, + "ApplicationLocalStates": { + "description": "Pair of application index and application local state", + "type": "object", + "required": [ + "id", + "state" + ], + "properties": { + "id": { + "type": "integer" + }, + "state": { + "$ref": "#/definitions/ApplicationLocalState" + } + } + }, + "ApplicationLocalState": { + "description": "Stores local state associated with an application.", + "type": "object", + "required": [ + "schema", + "key-value" + ], + "properties": { + "schema": { + "description": "\\[hsch\\] schema.", + "$ref": "#/definitions/ApplicationStateSchema" + }, + "key-value": { + "description": "\\[tkv\\] storage.", + "$ref": "#/definitions/TealKeyValueStore" + } + } + }, + "TealKeyValueStore": { + "description": "Represents a key-value store for use in an application.", + "type": "array", + "items": { + "$ref": "#/definitions/TealKeyValue" + } + }, + "TealKeyValue": { + "description": "Represents a key-value pair in an application store.", + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/TealValue" + } + } + }, + "TealValue": { + "description": "Represents a TEAL value.", + "type": "object", + "required": [ + "type", + "uint", + "bytes" + ], + "properties": { + "type": { + "description": "\\[tt\\] value type.", + "type": "integer" + }, + "bytes": { + "description": "\\[tb\\] bytes value.", + "type": "string" + }, + "uint": { + "description": "\\[ui\\] uint value.", + "type": "integer", + "x-algorand-format": "uint64" + } + } + }, + "StateDelta": { + "description": "Application state delta.", + "type": "array", + "items": { + "$ref": "#/definitions/EvalDeltaKeyValue" + } + }, + "AccountStateDelta": { + "description": "Application state delta.", + "type": "object", + "required": [ + "address", + "delta" + ], + "properties": { + "address": { + "type": "string" + }, + "delta": { + "$ref": "#/definitions/StateDelta" + } + } + }, + "EvalDeltaKeyValue": { + "description": "Key-value pairs for StateDelta.", + "type": "object", + "required": [ + "key", + "value" + ], + "properties": { + "key": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/EvalDelta" + } + } + }, + "EvalDelta": { + "description": "Represents a TEAL value delta.", + "type": "object", + "required": [ + "action" + ], + "properties": { + "action": { + "description": "\\[at\\] delta action.", + "type": "integer" + }, + "bytes": { + "description": "\\[bs\\] bytes value.", + "type": "string" + }, + "uint": { + "description": "\\[ui\\] uint value.", + "type": "integer", + "x-algorand-format": "uint64" + } + } + }, + "Application": { + "description": "Application index and its parameters", + "type": "object", + "required": [ + "id", + "params" + ], + "properties": { + "id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "params": { + "description": "\\[appparams\\] application parameters.", + "$ref": "#/definitions/ApplicationParams" + } + } + }, + "ApplicationParams": { + "description": "Stores the global information associated with an application.", + "type": "object", + "required": [ + "creator", + "approval-program", + "clear-state-program" + ], + "properties": { + "creator": { + "description": "The address that created this application. This is the address where the parameters and global state for this application can be found.", + "type": "string", + "x-algorand-format": "Address" + }, + "approval-program": { + "description": "\\[approv\\] approval program.", + "type": "string", + "format": "byte" + }, + "clear-state-program": { + "description": "\\[clearp\\] approval program.", + "type": "string", + "format": "byte" + }, + "local-state-schema": { + "description": "[\\lsch\\] local schema", + "$ref": "#/definitions/ApplicationStateSchema" + }, + "global-state-schema": { + "description": "[\\lsch\\] global schema", + "$ref": "#/definitions/ApplicationStateSchema" + }, + "global-state": { + "description": "[\\gs\\] global schema", + "$ref": "#/definitions/TealKeyValueStore" + } + } + }, + "DryrunState": { + "description": "Stores the TEAL eval step data", + "type": "object", + "required": [ + "line", + "pc", + "stack" + ], + "properties": { + "line": { + "description": "Line number", + "type": "integer" + }, + "pc": { + "description": "Program counter", + "type": "integer" + }, + "stack": { + "type": "array", + "items": { + "$ref": "#/definitions/TealValue" + } + }, + "scratch": { + "type": "array", + "items": { + "$ref": "#/definitions/TealValue" + } + }, + "error": { + "description": "Evaluation error if any", + "type": "string" + } + } + }, + "DryrunTxnResult": { + "description": "DryrunTxnResult contains any LogicSig or ApplicationCall program debug information and state updates from a dryrun.", + "type": "object", + "required": [ + "disassembly" + ], + "properties": { + "disassembly": { + "description": "Disassembled program line by line.", + "type": "array", + "items": { + "type": "string" + } + }, + "logic-sig-trace": { + "type": "array", + "items": { + "$ref": "#/definitions/DryrunState" + } + }, + "logic-sig-messages": { + "type": "array", + "items": { + "type": "string" + } + }, + "app-call-trace": { + "type": "array", + "items": { + "$ref": "#/definitions/DryrunState" + } + }, + "app-call-messages": { + "type": "array", + "items": { + "type": "string" + } + }, + "global-delta": { + "$ref": "#/definitions/StateDelta" + }, + "local-deltas": { + "type": "array", + "items": { + "$ref": "#/definitions/AccountStateDelta" + } + } + } + }, "ErrorResponse": { "description": "An error response with optional data field.", "type": "object", @@ -1160,6 +1674,88 @@ "type": "integer" } } + }, + "DryrunRequest": { + "description": "Request data type for dryrun endpoint. Given the Transactions and simulated ledger state upload, run TEAL scripts and return debugging information.", + "type": "object", + "required": [ + "txns", + "accounts", + "apps", + "protocol-version", + "round", + "latest-timestamp", + "sources" + ], + "properties": { + "txns": { + "type": "array", + "items": { + "description": "SignedTxn object. Must be canonically encoded.", + "type": "string", + "format": "json", + "x-algorand-format": "SignedTransaction" + } + }, + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/Account" + } + }, + "apps": { + "type": "array", + "items": { + "$ref": "#/definitions/Application" + } + }, + "protocol-version": { + "description": "ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in.", + "type": "string" + }, + "round": { + "description": "Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to.", + "type": "integer", + "x-algorand-format": "uint64" + }, + "latest-timestamp": { + "description": "LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to.", + "type": "integer", + "format": "int64" + }, + "sources": { + "type": "array", + "items": { + "$ref": "#/definitions/DryrunSource" + } + } + } + }, + "DryrunSource": { + "description": "DryrunSource is TEAL source text that gets uploaded, compiled, and inserted into transactions or application state.", + "type": "object", + "required": [ + "field-name", + "source", + "txn-index", + "app-index" + ], + "properties": { + "field-name": { + "description": "FieldName is what kind of sources this is. If lsig then it goes into the transactions[this.TxnIndex].LogicSig. If approv or clearp it goes into the Approval Program or Clear State Program of application[this.AppIndex].", + "type": "string" + }, + "source": { + "type": "string" + }, + "txn-index": { + "type": "integer" + }, + "app-index": { + "type": "integer", + "x-algorand-format": "uint64" + } + } } }, "parameters": { @@ -1359,7 +1955,7 @@ } } }, - "CatchpointStartResponse":{ + "CatchpointStartResponse": { "tags": [ "private" ], @@ -1627,23 +2223,61 @@ "x-go-package": "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" } }, - "PostCompileResponse": { + "ApplicationResponse": { + "description": "Application information", + "schema": { + "$ref": "#/definitions/Application" + } + }, + "AssetResponse": { + "description": "Asset information", + "schema": { + "$ref": "#/definitions/Asset" + } + }, + "CompileResponse": { "description": "Teal compile Result", "schema": { "type": "object", "required": [ - "hash", - "result" + "hash", + "result" ], "properties": { "hash": { "description": "base32 SHA512_256 of program bytes (Address style)", "type": "string" }, - "result": { - "description": "base64 encoded program bytes", - "type": "string" - } + "result": { + "description": "base64 encoded program bytes", + "type": "string" + } + } + } + }, + "DryrunResponse": { + "description": "DryrunResponse contains per-txn debug information from a dryrun.", + "schema": { + "type": "object", + "required": [ + "txns", + "protocol-version", + "error" + ], + "properties": { + "txns": { + "type": "array", + "items": { + "$ref": "#/definitions/DryrunTxnResult" + } + }, + "error": { + "type": "string" + }, + "protocol-version": { + "description": "Protocol version is the protocol version Dryrun was operated under.", + "type": "string" + } } } } diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index beb5cf8429..3c47d2fcec 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -232,6 +232,26 @@ }, "description": "(empty)" }, + "ApplicationResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + }, + "description": "Application information" + }, + "AssetResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset information" + }, "BlockResponse": { "content": { "application/json": { @@ -299,6 +319,60 @@ }, "description": "(empty)" }, + "CompileResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "hash": { + "description": "base32 SHA512_256 of program bytes (Address style)", + "type": "string" + }, + "result": { + "description": "base64 encoded program bytes", + "type": "string" + } + }, + "required": [ + "hash", + "result" + ], + "type": "object" + } + } + }, + "description": "Teal compile Result" + }, + "DryrunResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string" + }, + "protocol-version": { + "description": "Protocol version is the protocol version Dryrun was operated under.", + "type": "string" + }, + "txns": { + "items": { + "$ref": "#/components/schemas/DryrunTxnResult" + }, + "type": "array" + } + }, + "required": [ + "error", + "protocol-version", + "txns" + ], + "type": "object" + } + } + }, + "description": "DryrunResponse contains per-txn debug information from a dryrun." + }, "NodeStatusResponse": { "content": { "application/json": { @@ -459,30 +533,6 @@ }, "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**." }, - "PostCompileResponse": { - "content": { - "application/json": { - "schema": { - "properties": { - "hash": { - "description": "base32 SHA512_256 of program bytes (Address style)", - "type": "string" - }, - "result": { - "description": "base64 encoded program bytes", - "type": "string" - } - }, - "required": [ - "hash", - "result" - ], - "type": "object" - } - } - }, - "description": "Teal compile Result" - }, "PostTransactionsResponse": { "content": { "application/json": { @@ -597,6 +647,16 @@ "description": "specifies the amount of MicroAlgos in the account, without the pending rewards.", "type": "integer" }, + "apps-local-state": { + "description": "\\[appl\\] applications local data stored in this account.\n\nNote the raw object uses `map[int] -> AppLocalState` for this type.", + "items": { + "$ref": "#/components/schemas/ApplicationLocalStates" + }, + "type": "array" + }, + "apps-total-schema": { + "$ref": "#/components/schemas/ApplicationStateSchema" + }, "assets": { "description": "\\[asset\\] assets held by this account.\n\nNote the raw object uses `map[int] -> AssetHolding` for this type.", "items": { @@ -609,6 +669,13 @@ "type": "string", "x-algorand-format": "Address" }, + "created-apps": { + "description": "\\[appp\\] parameters of applications created by this account including app global data.\n\nNote: the raw account uses `map[int] -> AppParams` for this type.", + "items": { + "$ref": "#/components/schemas/Application" + }, + "type": "array" + }, "created-assets": { "description": "\\[apar\\] parameters of assets created by this account.\n\nNote: the raw account uses `map[int] -> Asset` for this type.", "items": { @@ -697,6 +764,126 @@ ], "type": "object" }, + "AccountStateDelta": { + "description": "Application state delta.", + "properties": { + "address": { + "type": "string" + }, + "delta": { + "$ref": "#/components/schemas/StateDelta" + } + }, + "required": [ + "address", + "delta" + ], + "type": "object" + }, + "Application": { + "description": "Application index and its parameters", + "properties": { + "id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "params": { + "$ref": "#/components/schemas/ApplicationParams" + } + }, + "required": [ + "id", + "params" + ], + "type": "object" + }, + "ApplicationLocalState": { + "description": "Stores local state associated with an application.", + "properties": { + "key-value": { + "$ref": "#/components/schemas/TealKeyValueStore" + }, + "schema": { + "$ref": "#/components/schemas/ApplicationStateSchema" + } + }, + "required": [ + "key-value", + "schema" + ], + "type": "object" + }, + "ApplicationLocalStates": { + "description": "Pair of application index and application local state", + "properties": { + "id": { + "type": "integer" + }, + "state": { + "$ref": "#/components/schemas/ApplicationLocalState" + } + }, + "required": [ + "id", + "state" + ], + "type": "object" + }, + "ApplicationParams": { + "description": "Stores the global information associated with an application.", + "properties": { + "approval-program": { + "description": "\\[approv\\] approval program.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "clear-state-program": { + "description": "\\[clearp\\] approval program.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "creator": { + "description": "The address that created this application. This is the address where the parameters and global state for this application can be found.", + "type": "string", + "x-algorand-format": "Address" + }, + "global-state": { + "$ref": "#/components/schemas/TealKeyValueStore" + }, + "global-state-schema": { + "$ref": "#/components/schemas/ApplicationStateSchema" + }, + "local-state-schema": { + "$ref": "#/components/schemas/ApplicationStateSchema" + } + }, + "required": [ + "approval-program", + "clear-state-program", + "creator" + ], + "type": "object" + }, + "ApplicationStateSchema": { + "description": "Specifies maximums on the number of each type that may be stored.", + "properties": { + "num-byte-slice": { + "description": "\\[nbs\\] num of byte slices.", + "type": "integer" + }, + "num-uint": { + "description": "\\[nui\\] num of uints.", + "type": "integer" + } + }, + "required": [ + "num-byte-slice", + "num-uint" + ], + "type": "object" + }, "Asset": { "description": "Specifies both the unique identifier and the parameters for an asset", "properties": { @@ -796,30 +983,288 @@ "description": "\\[un\\] Name of a unit of this asset, as supplied by the creator.", "type": "string" }, - "url": { - "description": "\\[au\\] URL where more information about the asset can be retrieved.", - "type": "string" + "url": { + "description": "\\[au\\] URL where more information about the asset can be retrieved.", + "type": "string" + } + }, + "required": [ + "creator", + "decimals", + "total" + ], + "type": "object" + }, + "DryrunRequest": { + "description": "Request data type for dryrun endpoint. Given the Transactions and simulated ledger state upload, run TEAL scripts and return debugging information.", + "properties": { + "accounts": { + "items": { + "$ref": "#/components/schemas/Account" + }, + "type": "array" + }, + "apps": { + "items": { + "$ref": "#/components/schemas/Application" + }, + "type": "array" + }, + "latest-timestamp": { + "description": "LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to.", + "format": "int64", + "type": "integer" + }, + "protocol-version": { + "description": "ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in.", + "type": "string" + }, + "round": { + "description": "Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to.", + "type": "integer", + "x-algorand-format": "uint64" + }, + "sources": { + "items": { + "$ref": "#/components/schemas/DryrunSource" + }, + "type": "array" + }, + "txns": { + "items": { + "description": "SignedTxn object. Must be canonically encoded.", + "format": "json", + "type": "string", + "x-algorand-format": "SignedTransaction" + }, + "type": "array" + } + }, + "required": [ + "accounts", + "apps", + "latest-timestamp", + "protocol-version", + "round", + "sources", + "txns" + ], + "type": "object" + }, + "DryrunSource": { + "description": "DryrunSource is TEAL source text that gets uploaded, compiled, and inserted into transactions or application state.", + "properties": { + "app-index": { + "type": "integer", + "x-algorand-format": "uint64" + }, + "field-name": { + "description": "FieldName is what kind of sources this is. If lsig then it goes into the transactions[this.TxnIndex].LogicSig. If approv or clearp it goes into the Approval Program or Clear State Program of application[this.AppIndex].", + "type": "string" + }, + "source": { + "type": "string" + }, + "txn-index": { + "type": "integer" + } + }, + "required": [ + "app-index", + "field-name", + "source", + "txn-index" + ], + "type": "object" + }, + "DryrunState": { + "description": "Stores the TEAL eval step data", + "properties": { + "error": { + "description": "Evaluation error if any", + "type": "string" + }, + "line": { + "description": "Line number", + "type": "integer" + }, + "pc": { + "description": "Program counter", + "type": "integer" + }, + "scratch": { + "items": { + "$ref": "#/components/schemas/TealValue" + }, + "type": "array" + }, + "stack": { + "items": { + "$ref": "#/components/schemas/TealValue" + }, + "type": "array" + } + }, + "required": [ + "line", + "pc", + "stack" + ], + "type": "object" + }, + "DryrunTxnResult": { + "description": "DryrunTxnResult contains any LogicSig or ApplicationCall program debug information and state updates from a dryrun.", + "properties": { + "app-call-messages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "app-call-trace": { + "items": { + "$ref": "#/components/schemas/DryrunState" + }, + "type": "array" + }, + "disassembly": { + "description": "Disassembled program line by line.", + "items": { + "type": "string" + }, + "type": "array" + }, + "global-delta": { + "$ref": "#/components/schemas/StateDelta" + }, + "local-deltas": { + "items": { + "$ref": "#/components/schemas/AccountStateDelta" + }, + "type": "array" + }, + "logic-sig-messages": { + "items": { + "type": "string" + }, + "type": "array" + }, + "logic-sig-trace": { + "items": { + "$ref": "#/components/schemas/DryrunState" + }, + "type": "array" + } + }, + "required": [ + "disassembly" + ], + "type": "object" + }, + "ErrorResponse": { + "description": "An error response with optional data field.", + "properties": { + "data": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + }, + "EvalDelta": { + "description": "Represents a TEAL value delta.", + "properties": { + "action": { + "description": "\\[at\\] delta action.", + "type": "integer" + }, + "bytes": { + "description": "\\[bs\\] bytes value.", + "type": "string" + }, + "uint": { + "description": "\\[ui\\] uint value.", + "type": "integer", + "x-algorand-format": "uint64" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "EvalDeltaKeyValue": { + "description": "Key-value pairs for StateDelta.", + "properties": { + "key": { + "type": "string" + }, + "value": { + "$ref": "#/components/schemas/EvalDelta" + } + }, + "required": [ + "key", + "value" + ], + "type": "object" + }, + "StateDelta": { + "description": "Application state delta.", + "items": { + "$ref": "#/components/schemas/EvalDeltaKeyValue" + }, + "type": "array" + }, + "TealKeyValue": { + "description": "Represents a key-value pair in an application store.", + "properties": { + "key": { + "type": "string" + }, + "value": { + "$ref": "#/components/schemas/TealValue" } }, "required": [ - "creator", - "decimals", - "total" + "key", + "value" ], "type": "object" }, - "ErrorResponse": { - "description": "An error response with optional data field.", + "TealKeyValueStore": { + "description": "Represents a key-value store for use in an application.", + "items": { + "$ref": "#/components/schemas/TealKeyValue" + }, + "type": "array" + }, + "TealValue": { + "description": "Represents a TEAL value.", "properties": { - "data": { + "bytes": { + "description": "\\[tb\\] bytes value.", "type": "string" }, - "message": { - "type": "string" + "type": { + "description": "\\[tt\\] value type.", + "type": "integer" + }, + "uint": { + "description": "\\[ui\\] uint value.", + "type": "integer", + "x-algorand-format": "uint64" } }, "required": [ - "message" + "bytes", + "type", + "uint" ], "type": "object" }, @@ -978,6 +1423,18 @@ "description": "Given a specific account public key, this call returns the accounts status, balance and spendable amounts", "operationId": "AccountInformation", "parameters": [ + { + "description": "Configures whether the response object is JSON or MessagePack encoded.", + "in": "query", + "name": "format", + "schema": { + "enum": [ + "json", + "msgpack" + ], + "type": "string" + } + }, { "description": "An account public key", "in": "path", @@ -996,6 +1453,11 @@ "schema": { "$ref": "#/components/schemas/Account" } + }, + "application/msgpack": { + "schema": { + "$ref": "#/components/schemas/Account" + } } }, "description": "(empty)" @@ -1006,6 +1468,11 @@ "schema": { "$ref": "#/components/schemas/ErrorResponse" } + }, + "application/msgpack": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } }, "description": "Malformed address" @@ -1016,6 +1483,11 @@ "schema": { "$ref": "#/components/schemas/ErrorResponse" } + }, + "application/msgpack": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } }, "description": "Invalid API Token" @@ -1026,6 +1498,11 @@ "schema": { "$ref": "#/components/schemas/ErrorResponse" } + }, + "application/msgpack": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } } }, "description": "Internal Error" @@ -1153,6 +1630,154 @@ "summary": "Get a list of unconfirmed transactions currently in the transaction pool by address." } }, + "/v2/applications/{application-id}": { + "get": { + "description": "Given a application id, it returns application information including creator, approval and clear programs, global and local schemas, and global state.", + "operationId": "GetApplicationByID", + "parameters": [ + { + "description": "An application identifier", + "in": "path", + "name": "application-id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Application" + } + } + }, + "description": "Application information" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid API Token" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Application Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Error" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Get application information." + } + }, + "/v2/assets/{asset-id}": { + "get": { + "description": "Given a asset id, it returns asset information including creator, name, total supply and special addresses.", + "operationId": "GetAssetByID", + "parameters": [ + { + "description": "An asset identifier", + "in": "path", + "name": "asset-id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Asset" + } + } + }, + "description": "Asset information" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid API Token" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Application Not Found" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Error" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Get asset information." + } + }, "/v2/blocks/{round}": { "get": { "operationId": "GetBlock", @@ -1950,6 +2575,96 @@ "x-codegen-request-body-name": "source" } }, + "/v2/teal/dryrun": { + "post": { + "description": "Executes TEAL program(s) in context and returns debugging information about the execution.", + "operationId": "TealDryrun", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DryrunRequest" + } + }, + "application/msgpack": { + "schema": { + "$ref": "#/components/schemas/DryrunRequest" + } + } + }, + "description": "Transaction (or group) and any accompanying state-simulation data.", + "required": false + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "error": { + "type": "string" + }, + "protocol-version": { + "description": "Protocol version is the protocol version Dryrun was operated under.", + "type": "string" + }, + "txns": { + "items": { + "$ref": "#/components/schemas/DryrunTxnResult" + }, + "type": "array" + } + }, + "required": [ + "error", + "protocol-version", + "txns" + ], + "type": "object" + } + } + }, + "description": "DryrunResponse contains per-txn debug information from a dryrun." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Bad Request" + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Invalid API Token" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + }, + "description": "Internal Error" + }, + "default": { + "content": {}, + "description": "Unknown Error" + } + }, + "summary": "Provide debugging information for a transaction (or group).", + "x-codegen-request-body-name": "request" + } + }, "/v2/transactions": { "post": { "operationId": "RawTransaction", diff --git a/daemon/algod/api/client/restClient.go b/daemon/algod/api/client/restClient.go index b36381d711..9db7b17d51 100644 --- a/daemon/algod/api/client/restClient.go +++ b/daemon/algod/api/client/restClient.go @@ -60,6 +60,7 @@ const ( // rawRequestPaths is a set of paths where the body should not be urlencoded var rawRequestPaths = map[string]bool{ "/v1/transactions": true, + "/v2/teal/dryrun": true, "/v2/teal/compile": true, } @@ -289,10 +290,19 @@ type assetsParams struct { Max uint64 `url:"max"` } +type appsParams struct { + AppIdx uint64 `url:"appIdx"` + Max uint64 `url:"max"` +} + type rawblockParams struct { Raw uint64 `url:"raw"` } +type rawAccountParams struct { + Format string `url:"format"` +} + // TransactionsByAddr returns all transactions for a PK [addr] in the [first, // last] rounds range. func (client RestClient) TransactionsByAddr(addr string, first, last, max uint64) (response v1.TransactionList, err error) { @@ -312,12 +322,47 @@ func (client RestClient) Assets(assetIdx, max uint64) (response v1.AssetList, er return } +// AssetInformationV2 gets the AssetInformationResponse associated with the passed asset index +func (client RestClient) AssetInformationV2(index uint64) (response generatedV2.Asset, err error) { + err = client.get(&response, fmt.Sprintf("/v2/assets/%d", index), nil) + return +} + +// ApplicationInformation gets the ApplicationInformationResponse associated +// with the passed application index +func (client RestClient) ApplicationInformation(index uint64) (response generatedV2.Application, err error) { + err = client.get(&response, fmt.Sprintf("/v2/applications/%d", index), nil) + return +} + // AccountInformation also gets the AccountInformationResponse associated with the passed address func (client RestClient) AccountInformation(address string) (response v1.Account, err error) { err = client.get(&response, fmt.Sprintf("/v1/account/%s", address), nil) return } +// AccountInformationV2 gets the AccountData associated with the passed address +func (client RestClient) AccountInformationV2(address string) (response generatedV2.Account, err error) { + err = client.get(&response, fmt.Sprintf("/v2/accounts/%s", address), nil) + return +} + +// Blob represents arbitrary blob of data satisfying v1.RawResponse interface +type Blob []byte + +// SetBytes fulfills the RawResponse interface on Blob +func (blob *Blob) SetBytes(b []byte) { + *blob = b +} + +// RawAccountInformationV2 gets the raw AccountData associated with the passed address +func (client RestClient) RawAccountInformationV2(address string) (response []byte, err error) { + var blob Blob + err = client.getRaw(&blob, fmt.Sprintf("/v2/accounts/%s", address), rawAccountParams{Format: "msgpack"}) + response = blob + return +} + // TransactionInformation gets information about a specific transaction involving a specific account func (client RestClient) TransactionInformation(accountAddress, transactionID string) (response v1.Transaction, err error) { transactionID = stripTransaction(transactionID) @@ -415,7 +460,7 @@ func (client RestClient) GetGoRoutines(ctx context.Context) (goRoutines string, // Compile compiles the given program and returned the compiled program func (client RestClient) Compile(program []byte) (compiledProgram []byte, programHash crypto.Digest, err error) { - var compileResponse generatedV2.PostCompileResponse + var compileResponse generatedV2.CompileResponse err = client.submitForm(&compileResponse, "/v2/teal/compile", program, "POST", false, true) if err != nil { return nil, crypto.Digest{}, err @@ -468,3 +513,11 @@ func (client RestClient) doGetWithQuery(ctx context.Context, path string, queryA result = string(bytes) return } + +// RawDryrun gets the raw DryrunResponse associated with the passed address +func (client RestClient) RawDryrun(data []byte) (response []byte, err error) { + var blob Blob + err = client.submitForm(&blob, "/v2/teal/dryrun", data, "POST", false /* encodeJSON */, false /* decodeJSON */) + response = blob + return +} diff --git a/daemon/algod/api/server/v1/handlers/errors.go b/daemon/algod/api/server/v1/handlers/errors.go index e1ebfb3525..4dbf78aafc 100644 --- a/daemon/algod/api/server/v1/handlers/errors.go +++ b/daemon/algod/api/server/v1/handlers/errors.go @@ -26,12 +26,18 @@ var ( errFailedParsingRoundNumber = "failed to parse the round number" errFailedParsingRawOption = "failed to parse the raw option" errFailedParsingMaxAssetsToList = "failed to parse max assets, must be between %d and %d" + errFailedParsingMaxAppsToList = "failed to parse max applications, must be between %d and %d" errFailedParsingAssetIdx = "failed to parse asset index" + errFailedParsingAppIdx = "failed to parse app index" errFailedToGetAssetCreator = "failed to retrieve asset creator from the ledger" + errFailedToGetAppCreator = "failed to retrieve app creator from the ledger" + errAppDoesNotExist = "application does not exist" + errFailedRetrievingApp = "failed to retrieve application information" errFailedToParseAddress = "failed to parse the address" errFailedToParseTransaction = "failed to parse transaction" errFailedToParseMaxValue = "failed to parse max value" errFailedToParseAssetIndex = "failed to parse asset index" + errFailedToParseAppIndex = "failed to parse application index" errInternalFailure = "internal failure" errIndexerNotRunning = "indexer isn't running, this call is disabled" errInvalidTransactionTypeLedger = "a transaction with invalid type field was found in ledger - type %s, transaction #%s, round %d" diff --git a/daemon/algod/api/server/v1/handlers/handlers.go b/daemon/algod/api/server/v1/handlers/handlers.go index d1f12b407b..3734aea644 100644 --- a/daemon/algod/api/server/v1/handlers/handlers.go +++ b/daemon/algod/api/server/v1/handlers/handlers.go @@ -17,6 +17,7 @@ package handlers import ( + "encoding/base64" "errors" "fmt" "io" @@ -31,7 +32,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/server/lib" - "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/data" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" @@ -88,6 +89,8 @@ func txEncode(tx transactions.Transaction, ad transactions.ApplyData) (v1.Transa res = assetTransferTxEncode(tx, ad) case protocol.AssetFreezeTx: res = assetFreezeTxEncode(tx, ad) + case protocol.ApplicationCallTx: + res = applicationCallTxEncode(tx, ad) default: return res, errors.New(errUnknownTransactionType) } @@ -157,7 +160,7 @@ func participationKeysEncode(r basics.AccountData) *v1.Participation { return &apiParticipation } -func assetParams(creator basics.Address, params basics.AssetParams) v1.AssetParams { +func modelAssetParams(creator basics.Address, params basics.AssetParams) v1.AssetParams { paramsModel := v1.AssetParams{ Total: params.Total, DefaultFrozen: params.DefaultFrozen, @@ -194,8 +197,55 @@ func assetParams(creator basics.Address, params basics.AssetParams) v1.AssetPara return paramsModel } +func modelSchema(schema basics.StateSchema) *v1.StateSchema { + return &v1.StateSchema{ + NumUint: schema.NumUint, + NumByteSlice: schema.NumByteSlice, + } +} + +func modelValue(v basics.TealValue) v1.TealValue { + return v1.TealValue{ + Type: v.Type.String(), + Bytes: base64.StdEncoding.EncodeToString([]byte(v.Bytes)), + Uint: v.Uint, + } +} + +func modelTealKeyValue(kv basics.TealKeyValue) map[string]v1.TealValue { + b64 := base64.StdEncoding + res := make(map[string]v1.TealValue, len(kv)) + for key, value := range kv { + kenc := b64.EncodeToString([]byte(key)) + res[kenc] = modelValue(value) + } + return res +} + +func modelAppParams(creator basics.Address, params basics.AppParams) v1.AppParams { + b64 := base64.StdEncoding + res := v1.AppParams{ + ApprovalProgram: b64.EncodeToString([]byte(params.ApprovalProgram)), + ClearStateProgram: b64.EncodeToString([]byte(params.ClearStateProgram)), + GlobalStateSchema: modelSchema(params.GlobalStateSchema), + LocalStateSchema: modelSchema(params.LocalStateSchema), + GlobalState: modelTealKeyValue(params.GlobalState), + } + if !creator.IsZero() { + res.Creator = creator.String() + } + return res +} + +func modelAppLocalState(s basics.AppLocalState) v1.AppLocalState { + return v1.AppLocalState{ + Schema: modelSchema(s.Schema), + KeyValue: modelTealKeyValue(s.KeyValue), + } +} + func assetConfigTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { - params := assetParams(basics.Address{}, tx.AssetConfigTxnFields.AssetParams) + params := modelAssetParams(basics.Address{}, tx.AssetConfigTxnFields.AssetParams) config := v1.AssetConfigTransactionType{ AssetID: uint64(tx.AssetConfigTxnFields.ConfigAsset), @@ -207,6 +257,40 @@ func assetConfigTxEncode(tx transactions.Transaction, ad transactions.ApplyData) } } +func applicationCallTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { + b64 := base64.StdEncoding + app := v1.ApplicationCallTransactionType{ + ApplicationID: uint64(tx.ApplicationID), + ApprovalProgram: b64.EncodeToString([]byte(tx.ApprovalProgram)), + ClearStateProgram: b64.EncodeToString([]byte(tx.ClearStateProgram)), + LocalStateSchema: modelSchema(tx.LocalStateSchema), + GlobalStateSchema: modelSchema(tx.GlobalStateSchema), + OnCompletion: tx.OnCompletion.String(), + } + + encodedAccounts := make([]string, 0, len(tx.Accounts)) + for _, addr := range tx.Accounts { + encodedAccounts = append(encodedAccounts, addr.String()) + } + + encodedForeignApps := make([]uint64, 0, len(tx.ForeignApps)) + for _, aidx := range tx.ForeignApps { + encodedForeignApps = append(encodedForeignApps, uint64(aidx)) + } + + encodedArgs := make([]string, 0, len(tx.ApplicationArgs)) + for _, arg := range tx.ApplicationArgs { + encodedArgs = append(encodedArgs, b64.EncodeToString([]byte(arg))) + } + + app.Accounts = encodedAccounts + app.ApplicationArgs = encodedArgs + app.ForeignApps = encodedForeignApps + return v1.Transaction{ + ApplicationCall: &app, + } +} + func assetTransferTxEncode(tx transactions.Transaction, ad transactions.ApplyData) v1.Transaction { xfer := v1.AssetTransferTransactionType{ AssetID: uint64(tx.AssetTransferTxnFields.XferAsset), @@ -250,7 +334,7 @@ func txWithStatusEncode(tr node.TxnWithStatus) (v1.Transaction, error) { return s, nil } -func computeAssetIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx uint64) { +func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx uint64) { // Compute transaction index in block offset := -1 for idx, stxnib := range payset { @@ -301,7 +385,42 @@ func computeAssetIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx uint6 return 0 } - return computeAssetIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) + return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) +} + +// computeAppIndexFromTxn returns the created app index given a confirmed +// transaction whose confirmation block is available in the ledger. Note that +// 0 is an invalid asset index (they start at 1). +func computeAppIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx uint64) { + // Must have ledger + if l == nil { + return 0 + } + // Transaction must be confirmed + if tx.ConfirmedRound == 0 { + return 0 + } + // Transaction must be ApplicationCall transaction + if tx.Txn.Txn.ApplicationCallTxnFields.Empty() { + return 0 + } + // Transaction must be creating an application + if tx.Txn.Txn.ApplicationCallTxnFields.ApplicationID != 0 { + return 0 + } + + // Look up block where transaction was confirmed + blk, err := l.Block(tx.ConfirmedRound) + if err != nil { + return 0 + } + + payset, err := blk.DecodePaysetFlat() + if err != nil { + return 0 + } + + return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) } func blockEncode(b bookkeeping.Block, c agreement.Certificate) (v1.Block, error) { @@ -605,14 +724,14 @@ func AccountInformation(ctx lib.ReqContext, context echo.Context) { return } - myLedger := ctx.Node.Ledger() - lastRound := myLedger.Latest() - record, err := myLedger.Lookup(lastRound, basics.Address(addr)) + ledger := ctx.Node.Ledger() + lastRound := ledger.Latest() + record, err := ledger.Lookup(lastRound, basics.Address(addr)) if err != nil { lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) return } - recordWithoutPendingRewards, err := myLedger.LookupWithoutRewards(lastRound, basics.Address(addr)) + recordWithoutPendingRewards, err := ledger.LookupWithoutRewards(lastRound, basics.Address(addr)) if err != nil { lib.ErrorResponse(w, http.StatusInternalServerError, err, errFailedLookingUpLedger, ctx.Log) return @@ -632,8 +751,8 @@ func AccountInformation(ctx lib.ReqContext, context echo.Context) { assets = make(map[uint64]v1.AssetHolding) for curid, holding := range record.Assets { var creator string - creatorAddr, err := myLedger.GetAssetCreator(curid) - if err == nil { + creatorAddr, ok, err := ledger.GetCreator(basics.CreatableIndex(curid), basics.AssetCreatable) + if err == nil && ok { creator = creatorAddr.String() } else { // Asset may have been deleted, so we can no @@ -648,11 +767,27 @@ func AccountInformation(ctx lib.ReqContext, context echo.Context) { } } - var thisAssetParams map[uint64]v1.AssetParams + var assetParams map[uint64]v1.AssetParams if len(record.AssetParams) > 0 { - thisAssetParams = make(map[uint64]v1.AssetParams) + assetParams = make(map[uint64]v1.AssetParams, len(record.AssetParams)) for idx, params := range record.AssetParams { - thisAssetParams[uint64(idx)] = assetParams(addr, params) + assetParams[uint64(idx)] = modelAssetParams(addr, params) + } + } + + var apps map[uint64]v1.AppLocalState + if len(record.AppLocalStates) > 0 { + apps = make(map[uint64]v1.AppLocalState, len(record.AppLocalStates)) + for idx, state := range record.AppLocalStates { + apps[uint64(idx)] = modelAppLocalState(state) + } + } + + var appParams map[uint64]v1.AppParams + if len(record.AppParams) > 0 { + appParams = make(map[uint64]v1.AppParams, len(record.AppParams)) + for idx, params := range record.AppParams { + appParams[uint64(idx)] = modelAppParams(addr, params) } } @@ -670,8 +805,10 @@ func AccountInformation(ctx lib.ReqContext, context echo.Context) { Rewards: record.RewardedMicroAlgos.Raw, Status: record.Status.String(), Participation: apiParticipation, - AssetParams: thisAssetParams, + AssetParams: assetParams, Assets: assets, + AppParams: appParams, + AppLocalStates: apps, } SendJSON(AccountInformationResponse{&accountInfo}, w, ctx.Log) @@ -852,10 +989,11 @@ func PendingTransactionInformation(ctx lib.ReqContext, context echo.Context) { responseTxs.TransactionResults = &v1.TransactionResults{ // This field will be omitted for transactions that did not - // create an asset (or for which we could not look up the block - // it was created in), because computeAssetIndexFromTxn will - // return 0 in that case. + // create an app/asset (or for which we could not look up the + // block it was created in), because compute{App|Asset}IndexFromTxn + // will return 0 in that case. CreatedAssetIndex: computeAssetIndexFromTxn(txn, ledger), + CreatedAppIndex: computeAppIndexFromTxn(txn, ledger), } response := TransactionResponse{ @@ -1110,8 +1248,10 @@ func AssetInformation(ctx lib.ReqContext, context echo.Context) { ledger := ctx.Node.Ledger() aidx := basics.AssetIndex(queryIndex) - creator, err := ledger.GetAssetCreator(aidx) - if err != nil { + creator, ok, err := ledger.GetCreator(basics.CreatableIndex(aidx), basics.AssetCreatable) + if err != nil || !ok { + // Treat a database error and a nonexistent application the + // same to avoid changing API behavior lib.ErrorResponse(w, http.StatusNotFound, err, errFailedToGetAssetCreator, ctx.Log) return } @@ -1124,7 +1264,7 @@ func AssetInformation(ctx lib.ReqContext, context echo.Context) { } if asset, ok := record.AssetParams[aidx]; ok { - thisAssetParams := assetParams(creator, asset) + thisAssetParams := modelAssetParams(creator, asset) SendJSON(AssetInformationResponse{&thisAssetParams}, w, ctx.Log) } else { lib.ErrorResponse(w, http.StatusBadRequest, fmt.Errorf(errFailedRetrievingAsset), errFailedRetrievingAsset, ctx.Log) @@ -1222,7 +1362,7 @@ func Assets(ctx lib.ReqContext, context echo.Context) { } // Append the result - params := assetParams(aloc.Creator, rp) + params := modelAssetParams(aloc.Creator, rp) result.Assets = append(result.Assets, v1.Asset{ AssetIndex: uint64(aloc.Index), AssetParams: params, diff --git a/daemon/algod/api/server/v1/handlers/responses.go b/daemon/algod/api/server/v1/handlers/responses.go index c608ed209e..bcf0ca747a 100644 --- a/daemon/algod/api/server/v1/handlers/responses.go +++ b/daemon/algod/api/server/v1/handlers/responses.go @@ -30,7 +30,7 @@ import ( "io" "net/http" - "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/logging" ) diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go new file mode 100644 index 0000000000..72774516ef --- /dev/null +++ b/daemon/algod/api/server/v2/account.go @@ -0,0 +1,354 @@ +// 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 . + +package v2 + +import ( + "errors" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" + "github.com/algorand/go-algorand/data/basics" +) + +// AccountDataToAccount converts basics.AccountData to v2.generated.Account +func AccountDataToAccount( + address string, record *basics.AccountData, assetsCreators map[basics.AssetIndex]string, + lastRound basics.Round, amountWithoutPendingRewards basics.MicroAlgos, +) (generated.Account, error) { + + assets := make([]generated.AssetHolding, 0, len(record.Assets)) + for curid, holding := range record.Assets { + // Empty is ok, asset may have been deleted, so we can no + // longer fetch the creator + creator := assetsCreators[curid] + holding := generated.AssetHolding{ + Amount: holding.Amount, + AssetId: uint64(curid), + Creator: creator, + IsFrozen: holding.Frozen, + } + + assets = append(assets, holding) + } + + createdAssets := make([]generated.Asset, 0, len(record.AssetParams)) + for idx, params := range record.AssetParams { + asset := AssetParamsToAsset(address, idx, ¶ms) + createdAssets = append(createdAssets, asset) + } + + var apiParticipation *generated.AccountParticipation + if record.VoteID != (crypto.OneTimeSignatureVerifier{}) { + apiParticipation = &generated.AccountParticipation{ + VoteParticipationKey: record.VoteID[:], + SelectionParticipationKey: record.SelectionID[:], + VoteFirstValid: uint64(record.VoteFirstValid), + VoteLastValid: uint64(record.VoteLastValid), + VoteKeyDilution: uint64(record.VoteKeyDilution), + } + } + + createdApps := make([]generated.Application, 0, len(record.AppParams)) + for appIdx, appParams := range record.AppParams { + app := AppParamsToApplication(address, appIdx, &appParams) + createdApps = append(createdApps, app) + } + + appsLocalState := make([]generated.ApplicationLocalStates, 0, len(record.AppLocalStates)) + for appIdx, state := range record.AppLocalStates { + localState := convertTKVToGenerated(&state.KeyValue) + appsLocalState = append(appsLocalState, generated.ApplicationLocalStates{ + Id: uint64(appIdx), + State: generated.ApplicationLocalState{ + KeyValue: localState, + Schema: generated.ApplicationStateSchema{ + NumByteSlice: state.Schema.NumByteSlice, + NumUint: state.Schema.NumUint, + }, + }, + }) + } + + totalAppSchema := generated.ApplicationStateSchema{ + NumByteSlice: record.TotalAppSchema.NumByteSlice, + NumUint: record.TotalAppSchema.NumUint, + } + + amount := record.MicroAlgos + pendingRewards, overflowed := basics.OSubA(amount, amountWithoutPendingRewards) + if overflowed { + return generated.Account{}, errors.New("overflow on pending reward calcuation") + } + + return generated.Account{ + SigType: nil, + Round: uint64(lastRound), + Address: address, + Amount: amount.Raw, + PendingRewards: pendingRewards.Raw, + AmountWithoutPendingRewards: amountWithoutPendingRewards.Raw, + Rewards: record.RewardedMicroAlgos.Raw, + Status: record.Status.String(), + RewardBase: &record.RewardsBase, + Participation: apiParticipation, + CreatedAssets: &createdAssets, + CreatedApps: &createdApps, + Assets: &assets, + AuthAddr: addrOrNil(record.AuthAddr), + AppsLocalState: &appsLocalState, + AppsTotalSchema: &totalAppSchema, + }, nil +} + +func convertTKVToGenerated(tkv *basics.TealKeyValue) (converted generated.TealKeyValueStore) { + for k, v := range *tkv { + converted = append(converted, generated.TealKeyValue{ + Key: k, + Value: generated.TealValue{ + Type: uint64(v.Type), + Bytes: v.Bytes, + Uint: v.Uint, + }, + }) + } + return +} + +func convertGeneratedTKV(akvs *generated.TealKeyValueStore) basics.TealKeyValue { + if akvs == nil || len(*akvs) == 0 { + return nil + } + + tkv := make(basics.TealKeyValue) + for _, kv := range *akvs { + tkv[kv.Key] = basics.TealValue{ + Type: basics.TealType(kv.Value.Type), + Uint: kv.Value.Uint, + Bytes: kv.Value.Bytes, + } + } + return tkv +} + +// AccountToAccountData converts v2.generated.Account to basics.AccountData +func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { + var voteID crypto.OneTimeSignatureVerifier + var selID crypto.VRFVerifier + var voteFirstValid basics.Round + var voteLastValid basics.Round + var voteKeyDilution uint64 + if a.Participation != nil { + copy(voteID[:], a.Participation.VoteParticipationKey) + copy(selID[:], a.Participation.SelectionParticipationKey) + voteFirstValid = basics.Round(a.Participation.VoteFirstValid) + voteLastValid = basics.Round(a.Participation.VoteLastValid) + voteKeyDilution = a.Participation.VoteKeyDilution + } + + var rewardsBase uint64 + if a.RewardBase != nil { + rewardsBase = *a.RewardBase + } + + var assetParams map[basics.AssetIndex]basics.AssetParams + if a.CreatedAssets != nil && len(*a.CreatedAssets) > 0 { + assetParams = make(map[basics.AssetIndex]basics.AssetParams, len(*a.CreatedAssets)) + for _, ca := range *a.CreatedAssets { + var metadataHash [32]byte + copy(metadataHash[:], *ca.Params.MetadataHash) + manager, err := basics.UnmarshalChecksumAddress(*ca.Params.Manager) + if err != nil { + return basics.AccountData{}, err + } + reserve, err := basics.UnmarshalChecksumAddress(*ca.Params.Reserve) + if err != nil { + return basics.AccountData{}, err + } + freeze, err := basics.UnmarshalChecksumAddress(*ca.Params.Freeze) + if err != nil { + return basics.AccountData{}, err + } + clawback, err := basics.UnmarshalChecksumAddress(*ca.Params.Clawback) + if err != nil { + return basics.AccountData{}, err + } + + assetParams[basics.AssetIndex(ca.Index)] = basics.AssetParams{ + Total: ca.Params.Total, + Decimals: uint32(ca.Params.Decimals), + DefaultFrozen: *ca.Params.DefaultFrozen, + UnitName: *ca.Params.UnitName, + AssetName: *ca.Params.Name, + URL: *ca.Params.Url, + MetadataHash: metadataHash, + Manager: manager, + Reserve: reserve, + Freeze: freeze, + Clawback: clawback, + } + } + } + var assets map[basics.AssetIndex]basics.AssetHolding + if a.Assets != nil && len(*a.Assets) > 0 { + assets = make(map[basics.AssetIndex]basics.AssetHolding, len(*a.Assets)) + for _, h := range *a.Assets { + assets[basics.AssetIndex(h.AssetId)] = basics.AssetHolding{ + Amount: h.Amount, + Frozen: h.IsFrozen, + } + } + } + + var appLocalStates map[basics.AppIndex]basics.AppLocalState + if a.AppsLocalState != nil && len(*a.AppsLocalState) > 0 { + appLocalStates = make(map[basics.AppIndex]basics.AppLocalState, len(*a.AppsLocalState)) + for _, ls := range *a.AppsLocalState { + appLocalStates[basics.AppIndex(ls.Id)] = basics.AppLocalState{ + Schema: basics.StateSchema{ + NumUint: ls.State.Schema.NumUint, + NumByteSlice: ls.State.Schema.NumByteSlice, + }, + KeyValue: convertGeneratedTKV(&ls.State.KeyValue), + } + } + } + + var appParams map[basics.AppIndex]basics.AppParams + if a.CreatedApps != nil && len(*a.CreatedApps) > 0 { + appParams = make(map[basics.AppIndex]basics.AppParams, len(*a.CreatedApps)) + for _, params := range *a.CreatedApps { + appParams[basics.AppIndex(params.Id)] = ApplicationParamsToAppParams(¶ms.Params) + } + } + + totalSchema := basics.StateSchema{} + if a.AppsTotalSchema != nil { + totalSchema.NumUint = a.AppsTotalSchema.NumUint + totalSchema.NumByteSlice = a.AppsTotalSchema.NumByteSlice + } + + status, err := basics.UnmarshalStatus(a.Status) + if err != nil { + return basics.AccountData{}, err + } + + ad := basics.AccountData{ + Status: status, + MicroAlgos: basics.MicroAlgos{Raw: a.Amount}, + RewardsBase: rewardsBase, + RewardedMicroAlgos: basics.MicroAlgos{Raw: a.Rewards}, + VoteID: voteID, + SelectionID: selID, + VoteFirstValid: voteFirstValid, + VoteLastValid: voteLastValid, + VoteKeyDilution: voteKeyDilution, + Assets: assets, + AppLocalStates: appLocalStates, + AppParams: appParams, + TotalAppSchema: totalSchema, + } + + if a.AuthAddr != nil { + authAddr, err := basics.UnmarshalChecksumAddress(*a.AuthAddr) + if err != nil { + return basics.AccountData{}, err + } + ad.AuthAddr = authAddr + } + if len(assetParams) > 0 { + ad.AssetParams = assetParams + } + if len(assets) > 0 { + ad.Assets = assets + } + if len(appLocalStates) > 0 { + ad.AppLocalStates = appLocalStates + } + if len(appParams) > 0 { + ad.AppParams = appParams + } + + return ad, nil +} + +// ApplicationParamsToAppParams converts generated.ApplicationParams to basics.AppParams +func ApplicationParamsToAppParams(gap *generated.ApplicationParams) basics.AppParams { + ap := basics.AppParams{ + ApprovalProgram: gap.ApprovalProgram, + ClearStateProgram: gap.ClearStateProgram, + } + if gap.LocalStateSchema != nil { + ap.LocalStateSchema = basics.StateSchema{ + NumUint: gap.LocalStateSchema.NumUint, + NumByteSlice: gap.LocalStateSchema.NumByteSlice, + } + } + if gap.GlobalStateSchema != nil { + ap.GlobalStateSchema = basics.StateSchema{ + NumUint: gap.GlobalStateSchema.NumUint, + NumByteSlice: gap.GlobalStateSchema.NumByteSlice, + } + } + ap.GlobalState = convertGeneratedTKV(gap.GlobalState) + + return ap +} + +// AppParamsToApplication converts basics.AppParams to generated.Application +func AppParamsToApplication(creator string, appIdx basics.AppIndex, appParams *basics.AppParams) generated.Application { + globalState := convertTKVToGenerated(&appParams.GlobalState) + return generated.Application{ + Id: uint64(appIdx), + Params: generated.ApplicationParams{ + Creator: creator, + ApprovalProgram: appParams.ApprovalProgram, + ClearStateProgram: appParams.ClearStateProgram, + GlobalState: &globalState, + LocalStateSchema: &generated.ApplicationStateSchema{ + NumByteSlice: appParams.LocalStateSchema.NumByteSlice, + NumUint: appParams.LocalStateSchema.NumUint, + }, + GlobalStateSchema: &generated.ApplicationStateSchema{ + NumByteSlice: appParams.GlobalStateSchema.NumByteSlice, + NumUint: appParams.GlobalStateSchema.NumUint, + }, + }, + } +} + +// AssetParamsToAsset converts basics.AssetParams to generated.Asset +func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.AssetParams) generated.Asset { + assetParams := generated.AssetParams{ + Creator: creator, + Total: params.Total, + Decimals: uint64(params.Decimals), + DefaultFrozen: ¶ms.DefaultFrozen, + MetadataHash: byteOrNil(params.MetadataHash[:]), + Name: strOrNil(params.AssetName), + UnitName: strOrNil(params.UnitName), + Url: strOrNil(params.URL), + Clawback: addrOrNil(params.Clawback), + Freeze: addrOrNil(params.Freeze), + Manager: addrOrNil(params.Manager), + Reserve: addrOrNil(params.Reserve), + } + + return generated.Asset{ + Index: uint64(idx), + Params: assetParams, + } +} diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go new file mode 100644 index 0000000000..42d0be0a59 --- /dev/null +++ b/daemon/algod/api/server/v2/account_test.go @@ -0,0 +1,100 @@ +// 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 . + +package v2 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/protocol" +) + +func TestAccount(t *testing.T) { + proto := config.Consensus[protocol.ConsensusFuture] + appIdx := basics.AppIndex(1) + round := basics.Round(2) + + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + } + a := basics.AccountData{ + Status: basics.Online, + MicroAlgos: basics.MicroAlgos{Raw: 80000000}, + RewardedMicroAlgos: basics.MicroAlgos{Raw: ^uint64(0)}, + RewardsBase: 0, + AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{ + appIdx: { + Schema: basics.StateSchema{NumUint: 10}, + KeyValue: basics.TealKeyValue{ + "uint": basics.TealValue{Type: basics.TealUintType, Uint: 2}, + "bytes": basics.TealValue{Type: basics.TealBytesType, Bytes: "value"}, + }, + }, + }, + } + b := a.WithUpdatedRewards(proto, 100) + + addr := basics.Address{}.String() + conv, err := AccountDataToAccount(addr, &b, map[basics.AssetIndex]string{}, round, a.MicroAlgos) + require.NoError(t, err) + require.Equal(t, conv.Address, addr) + require.Equal(t, conv.Amount, b.MicroAlgos.Raw) + require.Equal(t, conv.AmountWithoutPendingRewards, a.MicroAlgos.Raw) + require.NotNil(t, conv.CreatedApps) + require.Equal(t, 1, len(*conv.CreatedApps)) + app := (*conv.CreatedApps)[0] + require.Equal(t, uint64(appIdx), app.Id) + require.Equal(t, params.ApprovalProgram, app.Params.ApprovalProgram) + require.Equal(t, params.GlobalStateSchema.NumUint, app.Params.GlobalStateSchema.NumUint) + require.Equal(t, params.GlobalStateSchema.NumByteSlice, app.Params.GlobalStateSchema.NumByteSlice) + require.NotNil(t, conv.AppsLocalState) + require.Equal(t, 1, len(*conv.AppsLocalState)) + + ls := (*conv.AppsLocalState)[0] + require.Equal(t, uint64(appIdx), ls.Id) + require.Equal(t, uint64(10), ls.State.Schema.NumUint) + require.Equal(t, uint64(0), ls.State.Schema.NumByteSlice) + require.Equal(t, 2, len(ls.State.KeyValue)) + value1 := generated.TealKeyValue{ + Key: "uint", + Value: generated.TealValue{ + Type: uint64(basics.TealUintType), + Uint: 2, + }, + } + value2 := generated.TealKeyValue{ + Key: "bytes", + Value: generated.TealValue{ + Type: uint64(basics.TealBytesType), + Bytes: "value", + }, + } + require.Contains(t, ls.State.KeyValue, value1) + require.Contains(t, ls.State.KeyValue, value2) + + c, err := AccountToAccountData(&conv) + require.NoError(t, err) + require.Equal(t, b, c) +} diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go new file mode 100644 index 0000000000..e3ec740ddf --- /dev/null +++ b/daemon/algod/api/server/v2/dryrun.go @@ -0,0 +1,509 @@ +// 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 . + +package v2 + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" + "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/ledger" + "github.com/algorand/go-algorand/protocol" +) + +// DryrunRequest object uploaded to /v2/teal/dryrun +// It is the same as generated.DryrunRequest but Txns deserialized properly. +// Given the Transactions and simulated ledger state upload, run TEAL scripts and return debugging information. +// This is also used for msgp-decoding +type DryrunRequest struct { + // Txns is transactions to simulate + Txns []transactions.SignedTxn `codec:"txns"` // not supposed to be serialized + + // Optional, useful for testing Application Call txns. + Accounts []generated.Account `codec:"accounts"` + + Apps []generated.Application `codec:"apps"` + + // ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in. + ProtocolVersion string `codec:"protocol-version"` + + // Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to. + Round uint64 `codec:"round"` + + // LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to. + LatestTimestamp int64 `codec:"latest-timestamp"` + + Sources []generated.DryrunSource `codec:"sources"` +} + +// DryrunRequestFromGenerated converts generated.DryrunRequest to DryrunRequest field by fields +// and re-types Txns []transactions.SignedTxn +func DryrunRequestFromGenerated(gdr *generated.DryrunRequest) (dr DryrunRequest, err error) { + dr.Txns = make([]transactions.SignedTxn, 0, len(gdr.Txns)) + for _, raw := range gdr.Txns { + // no transactions.SignedTxn in OAS, map[string]interface{} is not good enough + // json.RawMessage does the job + var txn transactions.SignedTxn + err = protocol.DecodeJSON(raw, &txn) + if err != nil { + return + } + dr.Txns = append(dr.Txns, txn) + } + dr.Accounts = gdr.Accounts + dr.Apps = gdr.Apps + dr.ProtocolVersion = gdr.ProtocolVersion + dr.Round = gdr.Round + dr.LatestTimestamp = int64(gdr.LatestTimestamp) + dr.Sources = gdr.Sources + return +} + +func (dr *DryrunRequest) expandSources() error { + for i, s := range dr.Sources { + program, err := logic.AssembleString(s.Source) + if err != nil { + return fmt.Errorf("Dryrun Source[%d]: %v", i, err) + } + switch s.FieldName { + case "lsig": + dr.Txns[s.TxnIndex].Lsig.Logic = program + case "approv", "clearp": + for ai, app := range dr.Apps { + if app.Id == s.AppIndex { + switch s.FieldName { + case "approv": + dr.Apps[ai].Params.ApprovalProgram = program + case "clearp": + dr.Apps[ai].Params.ClearStateProgram = program + } + } + } + default: + return fmt.Errorf("Dryrun Source[%d]: bad field name %#v", i, s.FieldName) + } + } + return nil +} + +type dryrunDebugReceiver struct { + disassembly string + lines []string + history []generated.DryrunState + scratchActive []bool +} + +func (ddr *dryrunDebugReceiver) updateScratch() { + any := false + maxActive := -1 + lasti := len(ddr.history) - 1 + + if ddr.history[lasti].Scratch == nil { + return + } + + for i, sv := range *ddr.history[lasti].Scratch { + if sv.Type != uint64(basics.TealUintType) || sv.Uint != 0 { + any = true + maxActive = i + } + } + + if any { + if ddr.scratchActive == nil { + ddr.scratchActive = make([]bool, maxActive+1, 256) + } + for i := len(ddr.scratchActive); i <= maxActive; i++ { + sv := (*ddr.history[lasti].Scratch)[i] + active := sv.Type != uint64(basics.TealUintType) || sv.Uint != 0 + ddr.scratchActive = append(ddr.scratchActive, active) + } + } else { + if ddr.scratchActive != nil { + *ddr.history[lasti].Scratch = (*ddr.history[lasti].Scratch)[:len(ddr.scratchActive)] + } else { + ddr.history[lasti].Scratch = nil + return + } + } + + scratchlen := maxActive + 1 + if len(ddr.scratchActive) > scratchlen { + scratchlen = len(ddr.scratchActive) + } + + *ddr.history[lasti].Scratch = (*ddr.history[lasti].Scratch)[:scratchlen] + for i := range *ddr.history[lasti].Scratch { + if !ddr.scratchActive[i] { + (*ddr.history[lasti].Scratch)[i].Type = 0 + } + } +} + +func (ddr *dryrunDebugReceiver) stateToState(state *logic.DebugState) generated.DryrunState { + st := generated.DryrunState{ + Line: uint64(state.Line), + Pc: uint64(state.PC), + } + st.Stack = make([]generated.TealValue, len(state.Stack)) + for i, v := range state.Stack { + st.Stack[i] = generated.TealValue{ + Uint: v.Uint, + Bytes: v.Bytes, + Type: uint64(v.Type), + } + } + if len(state.Error) > 0 { + st.Error = new(string) + *st.Error = state.Error + } + + scratch := make([]generated.TealValue, len(state.Scratch)) + for i, v := range state.Scratch { + scratch[i] = generated.TealValue{ + Uint: v.Uint, + Bytes: v.Bytes, + Type: uint64(v.Type), + } + } + st.Scratch = &scratch + return st +} + +// Register is fired on program creation (DebuggerHook interface) +func (ddr *dryrunDebugReceiver) Register(state *logic.DebugState) error { + ddr.disassembly = state.Disassembly + ddr.lines = strings.Split(state.Disassembly, "\n") + return nil +} + +// Update is fired on every step (DebuggerHook interface) +func (ddr *dryrunDebugReceiver) Update(state *logic.DebugState) error { + st := ddr.stateToState(state) + ddr.history = append(ddr.history, st) + ddr.updateScratch() + return nil +} + +// Complete is called when the program exits (DebuggerHook interface) +func (ddr *dryrunDebugReceiver) Complete(state *logic.DebugState) error { + return ddr.Update(state) +} + +type dryrunLedger struct { + // inputs: + + dr *DryrunRequest + proto *config.ConsensusParams + + // intermediate state: + + // index into dr.Accounts[] + accountsIn map[basics.Address]int + + // index into dr.Apps[] + accountApps map[basics.Address]int + + // accounts that have been Put + accounts map[basics.Address]basics.BalanceRecord +} + +func (dl *dryrunLedger) init() error { + dl.accounts = make(map[basics.Address]basics.BalanceRecord) + dl.accountsIn = make(map[basics.Address]int) + dl.accountApps = make(map[basics.Address]int) + for i, acct := range dl.dr.Accounts { + xaddr, err := basics.UnmarshalChecksumAddress(acct.Address) + if err != nil { + return err + } + dl.accountsIn[xaddr] = i + } + for i, app := range dl.dr.Apps { + var addr basics.Address + if app.Params.Creator != "" { + var err error + addr, err = basics.UnmarshalChecksumAddress(app.Params.Creator) + if err != nil { + return err + } + } + dl.accountApps[addr] = i + } + return nil +} + +// transactions.Balances interface +func (dl *dryrunLedger) Round() basics.Round { + return basics.Round(dl.dr.Round) +} + +// transactions.Balances interface +func (dl *dryrunLedger) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + // first check accounts from a previous Put() + br, ok := dl.accounts[addr] + if ok { + return br, nil + } + // check accounts from debug records uploaded + any := false + out := basics.BalanceRecord{ + Addr: addr, + } + accti, ok := dl.accountsIn[addr] + if ok { + any = true + acct := dl.dr.Accounts[accti] + var err error + if out.AccountData, err = AccountToAccountData(&acct); err != nil { + return basics.BalanceRecord{}, err + } + if withPendingRewards { + out.MicroAlgos.Raw = acct.Amount + } else { + out.MicroAlgos.Raw = acct.AmountWithoutPendingRewards + } + } + appi, ok := dl.accountApps[addr] + if ok { + any = true + app := dl.dr.Apps[appi] + out.AppParams = make(map[basics.AppIndex]basics.AppParams) + out.AppParams[basics.AppIndex(app.Id)] = ApplicationParamsToAppParams(&app.Params) + } + if !any { + return basics.BalanceRecord{}, fmt.Errorf("no account for addr %s", addr.String()) + } + return out, nil +} + +// transactions.Balances interface +func (dl *dryrunLedger) Put(br basics.BalanceRecord) error { + if dl.accounts == nil { + dl.accounts = make(map[basics.Address]basics.BalanceRecord) + } + dl.accounts[br.Addr] = br + return nil +} + +// PutWithCreatable is like Put, but should be used when creating or deleting an asset or application. +func (dl *dryrunLedger) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { + return nil +} + +// GetCreator gets the address of the creator of an app or asset +func (dl *dryrunLedger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + switch ctype { + case basics.AssetCreatable: + for _, acct := range dl.dr.Accounts { + if acct.CreatedAssets == nil { + continue + } + for _, asset := range *acct.CreatedAssets { + if asset.Index == uint64(cidx) { + addr, err := basics.UnmarshalChecksumAddress(acct.Address) + return addr, true, err + } + } + } + return basics.Address{}, false, fmt.Errorf("no asset %d", cidx) + case basics.AppCreatable: + for _, app := range dl.dr.Apps { + if app.Id == uint64(cidx) { + var addr basics.Address + if app.Params.Creator != "" { + var err error + addr, err = basics.UnmarshalChecksumAddress(app.Params.Creator) + if err != nil { + return basics.Address{}, false, err + } + } + return addr, true, nil + } + } + return basics.Address{}, false, fmt.Errorf("no app %d", cidx) + } + return basics.Address{}, false, fmt.Errorf("unknown creatable type %d", ctype) +} + +// Move MicroAlgos from one account to another, doing all necessary overflow checking (convenience method) +// TODO: Does this need to be part of the balances interface, or can it just be implemented here as a function that calls Put and Get? +func (dl *dryrunLedger) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { + return nil +} + +// Balances correspond to a Round, which mean that they also correspond +// to a ConsensusParams. This returns those parameters. +func (dl *dryrunLedger) ConsensusParams() config.ConsensusParams { + return *dl.proto +} + +func makeAppLedger(dl *dryrunLedger, txn *transactions.Transaction, appIdx basics.AppIndex) (l logic.LedgerForLogic, err error) { + globals := ledger.AppTealGlobals{ + CurrentRound: basics.Round(dl.dr.Round), + LatestTimestamp: dl.dr.LatestTimestamp, + } + localSchema := basics.StateSchema{NumUint: 16, NumByteSlice: 16} + globalSchema := basics.StateSchema{NumUint: 64, NumByteSlice: 64} + schemas := basics.StateSchemas{LocalStateSchema: localSchema, GlobalStateSchema: globalSchema} + return ledger.MakeDebugAppLedger(dl, appIdx, schemas, globals) +} + +// unit-testable core of dryrun handler +func doDryrunRequest(dr *DryrunRequest, proto *config.ConsensusParams, response *generated.DryrunResponse) { + err := dr.expandSources() + if err != nil { + response.Error = err.Error() + return + } + dl := dryrunLedger{dr: dr, proto: proto} + err = dl.init() + if err != nil { + response.Error = err.Error() + return + } + response.Txns = make([]generated.DryrunTxnResult, len(dr.Txns)) + for ti, stxn := range dr.Txns { + ep := logic.EvalParams{ + Txn: &stxn, + Proto: proto, + TxnGroup: dr.Txns, + GroupIndex: ti, + //Logger: nil, // TODO: capture logs, send them back + } + var result *generated.DryrunTxnResult + if len(stxn.Lsig.Logic) > 0 { + var debug dryrunDebugReceiver + ep.Debugger = &debug + pass, err := logic.Eval(stxn.Lsig.Logic, ep) + result = new(generated.DryrunTxnResult) + var messages []string + result.Disassembly = debug.lines + result.LogicSigTrace = &debug.history + if pass { + messages = append(messages, "PASS") + } else { + messages = append(messages, "REJECT") + } + if err != nil { + messages = append(messages, err.Error()) + } + result.LogicSigMessages = &messages + } + if stxn.Txn.Type == protocol.ApplicationCallTx { + appIdx := stxn.Txn.ApplicationID + if appIdx == 0 { + creator := stxn.Txn.Sender.String() + // check and use the first entry in dr.Apps + if len(dr.Apps) > 0 && dr.Apps[0].Params.Creator == creator { + appIdx = basics.AppIndex(dr.Apps[0].Id) + } + } + + l, err := makeAppLedger(&dl, &stxn.Txn, appIdx) + if err != nil { + response.Error = err.Error() + return + } + ep.Ledger = l + var app basics.AppParams + ok := false + for _, appt := range dr.Apps { + if appt.Id == uint64(appIdx) { + app = ApplicationParamsToAppParams(&appt.Params) + ok = true + break + } + } + var messages []string + if result == nil { + result = new(generated.DryrunTxnResult) + } + if !ok { + messages = make([]string, 1) + messages[0] = fmt.Sprintf("uploaded state did not include app id %d referenced in txn[%d]", appIdx, ti) + } else { + var debug dryrunDebugReceiver + ep.Debugger = &debug + var program []byte + messages = make([]string, 1) + if stxn.Txn.OnCompletion == transactions.ClearStateOC { + program = app.ClearStateProgram + messages[0] = "ClearStateProgram" + } else { + program = app.ApprovalProgram + messages[0] = "ApprovalProgram" + } + pass, delta, err := logic.EvalStateful(program, ep) + result.Disassembly = debug.lines + result.AppCallTrace = &debug.history + result.GlobalDelta = StateDeltaToStateDelta(delta.GlobalDelta) + if len(delta.LocalDeltas) > 0 { + localDeltas := make([]generated.AccountStateDelta, len(delta.LocalDeltas)) + for k, v := range delta.LocalDeltas { + ldaddr, err := stxn.Txn.AddressByIndex(k, stxn.Txn.Sender) + if err != nil { + messages = append(messages, err.Error()) + } + localDeltas = append(localDeltas, generated.AccountStateDelta{ + Address: ldaddr.String(), + Delta: *StateDeltaToStateDelta(v), + }) + } + result.LocalDeltas = &localDeltas + } + if pass { + messages = append(messages, "PASS") + } else { + messages = append(messages, "REJECT") + } + if err != nil { + messages = append(messages, err.Error()) + } + } + result.AppCallMessages = &messages + } + response.Txns[ti] = *result + } +} + +// StateDeltaToStateDelta converts basics.StateDelta to generated.StateDelta +func StateDeltaToStateDelta(sd basics.StateDelta) *generated.StateDelta { + if len(sd) == 0 { + return nil + } + + gsd := make(generated.StateDelta, 0, len(sd)) + for k, v := range sd { + bytes := base64.StdEncoding.EncodeToString([]byte(v.Bytes)) + gsd = append(gsd, generated.EvalDeltaKeyValue{ + Key: k, + Value: generated.EvalDelta{ + Action: uint64(v.Action), + Uint: &v.Uint, + Bytes: &bytes, + }, + }) + } + + return &gsd +} diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go new file mode 100644 index 0000000000..c910d3448b --- /dev/null +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -0,0 +1,930 @@ +// 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 . + +package v2 + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" + "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/protocol" +) + +func unB64(x string) []byte { + out, err := base64.StdEncoding.DecodeString(x) + if err != nil { + panic(err) + } + return out +} + +func tvStr(tv generated.TealValue) string { + if tv.Type == uint64(basics.TealBytesType) { + return tv.Bytes + } else if tv.Type == uint64(basics.TealUintType) { + return strconv.FormatUint(tv.Uint, 10) + } + return "UNKNOWN TEAL VALUE" +} + +func dbStack(stack []generated.TealValue) string { + parts := make([]string, len(stack)) + for i, sv := range stack { + parts[i] = tvStr(sv) + } + return strings.Join(parts, " ") +} + +func logTrace(t *testing.T, lines []string, trace []generated.DryrunState) { + var disasm string + for _, ds := range trace { + var line string + if len(lines) > 0 { + disasm = strings.Join(lines, "\n") + t.Log(disasm) + line = lines[ds.Line] + } else { + line = "" + } + t.Logf("\tstack=[%s]", dbStack(ds.Stack)) + t.Logf("%s\t// line=%d pc=%d", line, ds.Line, ds.Pc) + } +} + +func logStateDelta(t *testing.T, sd generated.StateDelta) { + for _, vd := range sd { + t.Logf("\t%s: %#v", vd.Key, vd) + } +} + +func logResponse(t *testing.T, response *generated.DryrunResponse) { + t.Log(response.Error) + for i, rt := range response.Txns { + t.Logf("txn[%d]", i) + if rt.LogicSigTrace != nil && len(*rt.LogicSigTrace) > 0 { + t.Log("Logic Sig:") + logTrace(t, rt.Disassembly, *rt.LogicSigTrace) + if rt.LogicSigMessages != nil && len(*rt.LogicSigMessages) > 0 { + t.Log("Messages:") + for _, m := range *rt.LogicSigMessages { + t.Log(m) + } + } + } + if rt.AppCallTrace != nil && len(*rt.AppCallTrace) > 0 { + t.Log("App Call:") + logTrace(t, rt.Disassembly, *rt.AppCallTrace) + if rt.AppCallMessages != nil && len(*rt.AppCallMessages) > 0 { + t.Log("Messages:") + for _, m := range *rt.AppCallMessages { + t.Log(m) + } + } + } + if rt.GlobalDelta != nil && len(*rt.GlobalDelta) > 0 { + t.Log("Global delta") + logStateDelta(t, *rt.GlobalDelta) + } + if rt.LocalDeltas != nil { + for _, ld := range *rt.LocalDeltas { + addr := ld.Address + delta := ld.Delta + t.Logf("%s delta", addr) + logStateDelta(t, delta) + } + } + } +} + +func TestDryrunLogicSig(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + + dr.Txns = []transactions.SignedTxn{ + { + Lsig: transactions.LogicSig{ + Logic: unB64("AiABASI="), + }, + }, + // it doesn't actually care about any txn content + } + doDryrunRequest(&dr, &proto, &response) + checkLogicSigPass(t, &response) + if t.Failed() { + logResponse(t, &response) + } +} + +func TestDryrunLogicSigSource(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + + dr.Txns = []transactions.SignedTxn{{}} + dr.Sources = []generated.DryrunSource{ + { + Source: "int 1", + FieldName: "lsig", + TxnIndex: 0, + }, + } + doDryrunRequest(&dr, &proto, &response) + checkLogicSigPass(t, &response) + if t.Failed() { + logResponse(t, &response) + } +} + +const globalTestSource = `#pragma version 2 +// This program approves all transactions whose first arg is "hello" +// Then, accounts can write "foo": "bar" to the GlobalState by +// sending a transaction whose first argument is "write". Finally, +// accounts can send the args ["check", xyz] to confirm that the +// key at "foo" is equal to the second argument, xyz + +// If arg 0 is "hello" +txna ApplicationArgs 0 +byte base64 aGVsbG8= +== +bnz succeed + +// else + +// If arg 0 is "write" +txna ApplicationArgs 0 +byte base64 d3JpdGU= +== +bnz write + +// else + +// arg 0 must be "check" +txna ApplicationArgs 0 +byte base64 Y2hlY2s= +== + +// and arg 1 must be the value at "foo" +// Key "foo" +int 0 +byte base64 Zm9v +app_global_get_ex + +// Value must exist +int 0 +== +bnz fail + +// Value must equal arg +txna ApplicationArgs 1 +== +&& + +int 1 +bnz done + +write: +// Write to GlobalState + +// Key "foo" +byte base64 Zm9v + +// Value "bar" +byte base64 YmFy +app_global_put + +int 1 +bnz succeed + +succeed: +int 1 +int 1 +bnz done + +fail: +int 0 + +done: +` + +var globalTestProgram []byte + +const localStateCheckSource = `#pragma version 2 +// This program approves all transactions whose first arg is "hello" +// Then, accounts can write "foo": "bar" to their LocalState by +// sending a transaction whose first argument is "write". Finally, +// accounts can send the args ["check", xyz] to confirm that the +// key at "foo" is equal to the second argument, xyz + +// If arg 0 is "hello" +txna ApplicationArgs 0 +byte base64 aGVsbG8= +== +bnz succeed + +// else + +// If arg 0 is "write" +txna ApplicationArgs 0 +byte base64 d3JpdGU= +== +bnz write + +// else + +// arg 0 must be "check" +txna ApplicationArgs 0 +byte base64 Y2hlY2s= +== + +// and arg 1 must be the value at "foo" +// txn.Sender +int 0 + +// App ID (this app) +int 0 + +// Key "foo" +byte base64 Zm9v +app_local_get_ex + +// Value must exist +int 0 +== +bnz fail + +// Value must equal arg +txna ApplicationArgs 1 +== +&& + +int 1 +bnz done + +write: +// Write to our LocalState + +// txn.Sender +int 0 + +// Key "foo" +byte base64 Zm9v + +// Value "bar" +byte base64 YmFy +app_local_put + +int 1 +bnz succeed + +succeed: +int 1 +int 1 +bnz done + +fail: +int 0 +int 1 +bnz done + +done: +` + +var localStateCheckProg []byte + +func init() { + var err error + globalTestProgram, err = logic.AssembleString(globalTestSource) + if err != nil { + panic(err) + } + localStateCheckProg, err = logic.AssembleString(localStateCheckSource) + if err != nil { + panic(err) + } +} + +func checkLogicSigPass(t *testing.T, response *generated.DryrunResponse) { + if len(response.Txns) < 1 { + t.Error("no response txns") + } else if len(response.Txns) == 0 { + t.Error("response txns is nil") + } else if response.Txns[0].LogicSigMessages == nil || len(*response.Txns[0].LogicSigMessages) < 1 { + t.Error("no response lsig msg") + } else { + messages := *response.Txns[0].LogicSigMessages + assert.Equal(t, "PASS", messages[len(messages)-1]) + } +} + +func checkAppCallPass(t *testing.T, response *generated.DryrunResponse) { + if len(response.Txns) < 1 { + t.Error("no response txns") + } else if len(response.Txns) == 0 { + t.Error("response txns is nil") + } else if response.Txns[0].AppCallMessages == nil || len(*response.Txns[0].AppCallMessages) < 1 { + t.Error("no response app msg") + } else { + messages := *response.Txns[0].AppCallMessages + assert.GreaterOrEqual(t, len(messages), 1) + assert.Equal(t, "PASS", messages[len(messages)-1]) + } +} + +func TestDryrunGlobal1(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("write"), + }, + }, + }, + }, + } + gkv := generated.TealKeyValueStore{ + generated.TealKeyValue{ + Key: "foo", + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: globalTestProgram, + GlobalState: &gkv, + }, + }, + } + doDryrunRequest(&dr, &proto, &response) + checkAppCallPass(t, &response) + if t.Failed() { + logResponse(t, &response) + } +} + +func TestDryrunGlobal2(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("check"), + []byte("bar"), + }, + }, + }, + }, + } + gkv := generated.TealKeyValueStore{ + generated.TealKeyValue{ + Key: "foo", + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: globalTestProgram, + GlobalState: &gkv, + }, + }, + } + doDryrunRequest(&dr, &proto, &response) + if len(response.Txns) < 1 { + t.Error("no response txns") + } else if response.Txns[0].AppCallMessages == nil || len(*response.Txns[0].AppCallMessages) < 1 { + t.Error("no response lsig msg") + } else { + messages := *response.Txns[0].AppCallMessages + assert.Equal(t, "PASS", messages[len(messages)-1]) + } + if t.Failed() { + logResponse(t, &response) + } +} + +func TestDryrunLocal1(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("write"), + []byte("foo"), + }, + }, + }, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: localStateCheckProg, + }, + }, + } + dr.Accounts = []generated.Account{ + { + Status: "Online", + Address: basics.Address{}.String(), + AppsLocalState: &[]generated.ApplicationLocalStates{{Id: 1}}, + }, + } + doDryrunRequest(&dr, &proto, &response) + checkAppCallPass(t, &response) + if response.Txns[0].LocalDeltas == nil { + t.Fatal("empty local delta") + } + addrFound := false + valueFound := false + for _, lds := range *response.Txns[0].LocalDeltas { + if lds.Address == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" { + addrFound = true + for _, ld := range lds.Delta { + if ld.Key == "foo" { + valueFound = true + assert.Equal(t, ld.Value.Action, uint64(basics.SetBytesAction)) + assert.Equal(t, *ld.Value.Bytes, "YmFy") // bar + + } + } + } + } + if !addrFound { + t.Error("no local delta for AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ") + } + if !valueFound { + t.Error("no local delta for value foo") + } + if t.Failed() { + logResponse(t, &response) + } +} + +func TestDryrunLocal1A(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("write"), + []byte("foo"), + }, + }, + }, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + }, + } + dr.Accounts = []generated.Account{ + { + Status: "Online", + Address: basics.Address{}.String(), + AppsLocalState: &[]generated.ApplicationLocalStates{{Id: 1}}, + }, + } + + dr.Sources = []generated.DryrunSource{ + { + Source: localStateCheckSource, + FieldName: "approv", + AppIndex: 1, + }, + } + doDryrunRequest(&dr, &proto, &response) + checkAppCallPass(t, &response) + if response.Txns[0].LocalDeltas == nil { + t.Fatal("empty local delta") + } + addrFound := false + valueFound := false + for _, lds := range *response.Txns[0].LocalDeltas { + if lds.Address == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" { + addrFound = true + for _, ld := range lds.Delta { + if ld.Key == "foo" { + valueFound = true + assert.Equal(t, ld.Value.Action, uint64(basics.SetBytesAction)) + assert.Equal(t, *ld.Value.Bytes, "YmFy") // bar + + } + } + } + } + if !addrFound { + t.Error("no local delta for AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ") + } + if !valueFound { + t.Error("no local delta for value foo") + } + if t.Failed() { + logResponse(t, &response) + } +} + +func TestDryrunLocalCheck(t *testing.T) { + // {"txns":[{"lsig":{"l":"AiABASI="},"txn":{}}]} + t.Parallel() + var dr DryrunRequest + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApplicationArgs: [][]byte{ + []byte("check"), + []byte("bar"), + }, + }, + }, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: localStateCheckProg, + }, + }, + } + localv := make(generated.TealKeyValueStore, 1) + localv[0] = generated.TealKeyValue{ + Key: "foo", + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + } + + dr.Accounts = []generated.Account{ + { + Status: "Online", + Address: basics.Address{}.String(), + AppsLocalState: &[]generated.ApplicationLocalStates{ + { + Id: 1, + State: generated.ApplicationLocalState{ + KeyValue: localv, + }, + }, + }, + }, + } + + doDryrunRequest(&dr, &proto, &response) + checkAppCallPass(t, &response) +} +func TestDryrunEncodeDecode(t *testing.T) { + t.Parallel() + + var gdr generated.DryrunRequest + txns := []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApprovalProgram: []byte{1, 2, 3}, + ApplicationArgs: [][]byte{ + []byte("check"), + []byte("bar"), + }, + }, + }, + }, + } + for i := range txns { + enc := protocol.EncodeJSON(&txns[i]) + gdr.Txns = append(gdr.Txns, enc) + } + + gdr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: localStateCheckProg, + }, + }, + } + localv := make(generated.TealKeyValueStore, 1) + localv[0] = generated.TealKeyValue{ + Key: "foo", + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + } + + gdr.Accounts = []generated.Account{ + { + Status: "Online", + Address: basics.Address{}.String(), + AppsLocalState: &[]generated.ApplicationLocalStates{ + { + Id: 1, + State: generated.ApplicationLocalState{ + KeyValue: localv, + }, + }, + }, + }, + } + + // use protocol + encoded := protocol.EncodeJSON(&gdr) + var decoded generated.DryrunRequest + err := protocol.DecodeJSON(encoded, &decoded) + require.NoError(t, err) + require.Equal(t, gdr, decoded) + + buf := bytes.NewBuffer(encoded) + dec := protocol.NewJSONDecoder(buf) + decoded = generated.DryrunRequest{} + err = dec.Decode(&decoded) + require.NoError(t, err) + require.Equal(t, gdr, decoded) + + // use json + data, err := json.Marshal(&gdr) + require.NoError(t, err) + gdr = generated.DryrunRequest{} + err = json.Unmarshal(data, &gdr) + require.NoError(t, err) + + dr, err := DryrunRequestFromGenerated(&gdr) + require.NoError(t, err) + require.Equal(t, 1, len(dr.Txns)) + require.Equal(t, txns[0].Txn.ApplicationID, dr.Txns[0].Txn.ApplicationID) + require.Equal(t, txns[0].Txn.ApprovalProgram, dr.Txns[0].Txn.ApprovalProgram) + require.Equal(t, []byte{1, 2, 3}, dr.Txns[0].Txn.ApprovalProgram) + require.Equal(t, txns[0].Txn.ApplicationArgs, dr.Txns[0].Txn.ApplicationArgs) + + // use protocol msgp + dr1, err := DryrunRequestFromGenerated(&gdr) + require.NoError(t, err) + encoded, err = encode(protocol.CodecHandle, &dr) + encoded2 := protocol.EncodeReflect(&dr) + require.Equal(t, encoded, encoded2) + + buf = bytes.NewBuffer(encoded) + dec = protocol.NewDecoder(buf) + var dr2 DryrunRequest + err = dec.Decode(&dr2) + require.NoError(t, err) + require.Equal(t, dr1, dr2) + + dec = protocol.NewDecoder(buf) + dr2 = DryrunRequest{} + err = decode(protocol.CodecHandle, encoded, &dr2) + require.NoError(t, err) + require.Equal(t, dr1, dr2) + + dr2 = DryrunRequest{} + err = protocol.DecodeReflect(encoded, &dr2) + require.NoError(t, err) + require.Equal(t, dr1, dr2) +} + +func TestDryrunMakeLedger(t *testing.T) { + t.Parallel() + + var dr DryrunRequest + var proto config.ConsensusParams + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + proto.MaxAppKeyLen = 64 + proto.MaxAppBytesValueLen = 64 + + sender, err := basics.UnmarshalChecksumAddress("UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M") + require.NoError(t, err) + + dr.Txns = []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Header: transactions.Header{Sender: sender}, + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApplicationArgs: [][]byte{ + []byte("check"), + []byte("bar"), + }, + }, + }, + }, + } + dr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + Creator: sender.String(), + ApprovalProgram: localStateCheckProg, + }, + }, + } + dl := dryrunLedger{dr: &dr, proto: &proto} + err = dl.init() + require.NoError(t, err) + _, err = makeAppLedger(&dl, &dr.Txns[0].Txn, 1) + require.NoError(t, err) +} + +var dataJSON = []byte(`{ + "accounts": [ + { + "address": "UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M", + "amount": 5002280000000000, + "amount-without-pending-rewards": 5000000000000000, + "participation": { + "selection-participation-key": "tVDPagKEH1ch9q0jWwPdBIe13k2EbOw+0UTrfpKLqlU=", + "vote-first-valid": 0, + "vote-key-dilution": 10000, + "vote-last-valid": 3000000, + "vote-participation-key": "gBw6xPd3U4pLXaRkw1UC1wgvR51P5+aYQv5OADAFyOM=" + }, + "pending-rewards": 2280000000000, + "reward-base": 456, + "rewards": 2280000000000, + "round": 18241, + "status": "Online" + } + ], + "apps": [ + { + "id": 1380011588, + "params": { + "creator": "UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M", + "approval-program": "AiABASI=", + "clear-state-program": "AiABASI=", + "global-state-schema": { + "num-byte-slice": 5, + "num-uint": 5 + }, + "local-state-schema": { + "num-byte-slice": 5, + "num-uint": 5 + } + } + } + ], + "latest-timestamp": 1592537757, + "protocol-version": "future", + "round": 18241, + "sources": null, + "txns": [ + { + "txn": { + "apap": "AiABASI=", + "apgs": { + "nbs": 5, + "nui": 5 + }, + "apls": { + "nbs": 5, + "nui": 5 + }, + "apsu": "AiABASI=", + "fee": 1000, + "fv": 18242, + "gh": "ZIkPs8pTDxbRJsFB1yJ7gvnpDu0Q85FRkl2NCkEAQLU=", + "lv": 19242, + "note": "tjpNge78JD8=", + "snd": "UAPJE355K7BG7RQVMTZOW7QW4ICZJEIC3RZGYG5LSHZ65K6LCNFPJDSR7M", + "type": "appl" + } + } + ] +}`) + +func TestDryrunRequestJSON(t *testing.T) { + t.Parallel() + + var gdr generated.DryrunRequest + buf := bytes.NewBuffer(dataJSON) + dec := protocol.NewJSONDecoder(buf) + err := dec.Decode(&gdr) + require.NoError(t, err) + + dr, err := DryrunRequestFromGenerated(&gdr) + require.NoError(t, err) + require.Equal(t, 1, len(dr.Txns)) + require.Equal(t, 1, len(dr.Accounts)) + require.Equal(t, 1, len(dr.Apps)) + + var proto config.ConsensusParams + var response generated.DryrunResponse + + proto.LogicSigVersion = 2 + proto.LogicSigMaxCost = 1000 + + doDryrunRequest(&dr, &proto, &response) + checkAppCallPass(t, &response) + if t.Failed() { + logResponse(t, &response) + } +} diff --git a/daemon/algod/api/server/v2/errors.go b/daemon/algod/api/server/v2/errors.go index f2c5c1810d..86e8d33406 100644 --- a/daemon/algod/api/server/v2/errors.go +++ b/daemon/algod/api/server/v2/errors.go @@ -17,6 +17,8 @@ package v2 var ( + errAppDoesNotExist = "application does not exist" + errAssetDoesNotExist = "asset does not exist" errFailedLookingUpLedger = "failed to retrieve information from the ledger" errFailedLookingUpTransactionPool = "failed to retrieve information from the transaction pool" errFailedRetrievingNodeStatus = "failed retrieving node status" diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 9adbe275e5..9a413eaa22 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -235,105 +235,130 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+w8a3MbN5J/Bcfdqtg+DinZcnatKteeYueh28RxWcre3Vq+DTjTJBHNABMAI4rx6b9f", - "dQOYJ4akYq/3UrefbHGA7ka/0N1o4P0kVUWpJEhrJqfvJyXXvAALmv7iaaoqaROR4V8ZmFSL0golJ6fh", - "GzNWC7maTCcCfy25XU+mE8kLaMbg/OlEw8+V0JBNTq2uYDox6RoKjoDttsTRNaTbZKUSD+LMgTh/Obnb", - "8YFnmQZjhlR+L/MtEzLNqwyY1VwanuInwzbCrpldC8P8ZCYkUxKYWjK77gxmSwF5ZmZhkT9XoLetVXrk", - "40u6a0hMtMphSOcLVSyEhEAV1ETVAmFWsQyWNGjNLUMMSGsYaBUzwHW6Zkul95DqiGjTC7IqJqdvJwZk", - "BpqklYK4of8uNcAvkFiuV2An76axxS0t6MSKIrK0c899DabKrWE0lta4EjcgGc6ase8qY9kCGJfszVcv", - "2JMnT57hQgpuLWReyUZX1WBvr8lNn5xOMm4hfB7qGs9XSnOZJfX4N1+9IPwXfoGHjuLGQNxYzvALO385", - "toAwMaJCQlpYkRw62o8zIkbR/LyApdJwoEzc4I8qlDb+f6hUUm7TdamEtBG5MPrK3OeoD2tN3+XDagI6", - "40vklEagb4+SZ+/eH0+Pj+5+9/Ys+av/8+mTuwOX/6KGu4cD0YFppTXIdJusNHCyljWXQ3688fpg1qrK", - "M7bmNyR8XpCr93MZznWu84bnFeqJSLU6y1fKMO7VKIMlr3LLAmJWyRzdFELz2s6EYaVWNyKDbIred7MW", - "6Zql3DgQNI5tRJ6jDlYGsjFdi69uhzHdtVmCdP0qftCC/u8yo1nXHk7ALXmDJM2VgcSqPdtT2HG4zFh7", - "Q2n2KnO/zYpdroERcvzgNlvinUSdzvMtsyTXjHHDOAtb05SJJduqim1IOLm4pvl+Nci1giHTSDidfRSN", - "d4x9A2ZEmLdQKgcuiXnB7oYsk0uxqjQYtlmDXfs9T4MplTTA1OInSC2K/d8vvn/FlGbfgTF8Ba95es1A", - "piobl7FHGtvBfzIKBV6YVcnT6/h2nYtCREj+jt+KoiqYrIoFaJRX2B+sYhpspeUYQQ7iHj0r+O0Q6aWu", - "ZErCbdB2AjVUJWHKnG9n7HzJCn77/GjqyTGM5zkrQWZCrpi9laNBGuLeT16iVSWzA2IYiwJr7ZqmhFQs", - "BWSshrKDEo9mHz1C3o+eJrJqkROAjJJTY9lDjoTbiM6g6eIXVvIVtFRmxn7wnou+WnUNsnZwbLGlT6WG", - "G6EqU08aoZFQ7w6vpbKQlBqWIqJjF54d6D3cGO9eCx/gpEpaLiRk6HmJaGXBeaJRmloIdyczwy16wQ18", - "fjK2gTdfD5T+UvWlvlPiB0mbBiXOJCP7In71BhsPmzrzD0j+2riNWCXu54EgxeoSt5KlyGmb+QnlF9hQ", - "GXICHUaEjceIleS20nB6JR/hXyxhF5bLjOsMfyncT99VuRUXYoU/5e6nb9VKpBdiNcLMmtZoNkXTCvcP", - "wou7Y3sbTRq+Veq6KtsLSjtZ6WLLzl+OCdnBvK9intWpbDuruLwNmcZ9Z9jbWpAjRI7yruQ48Bq2GpBa", - "ni7pn9sl6RNf6l9izETN9TssVQN8leCN/w1/QlsHlwzwssxFypGbc9o3T9+3KPm9huXkdPK7eVMimbuv", - "Zu7hOoxdsT2AorTbh7j8L3KVXv8q3KVWJWgr3CoWCGeoIASerYFnoFnGLZ81uYQLL0bETBO/oXmUHICO", - "ePbv6T88Z/gZlY/bELVgxCYMxi6qVV/JMNBx7tNhwgEUgClWuNiGYUxyLypfNMidX6odyVvPlnd9aBGZ", - "fOnCKUYzwiJw6U2ydLZQ+tfpSS+llKxJARlHqHXQhyvvSpaGVmXi+RMJI92AHqCm6jb0Jm0O9cEfwquW", - "/jbcubD878Adg1A/Bne6gD4Rd16pDC4st5X5CIxpgIVgxJAlCensAR0+X6jKMs6kynCNODjOspFqB6VZ", - "lB3athTs2pnqAnD/THm1WluGG48acrBdTkl46niZkFmZkeCwjurdKIfOZdK5Bp5t2QJAMrXwEZiPDWmR", - "nBI3G2qyXmANWXXU0KGr1CoFYyBLfAF6L2mhmL3UqnCYRthEdBO9NRJmFFty/StptcryfA+dNGZIrWkc", - "r49ah1Qfhn6X/PrI21LEHD0YFHp53ChzsDDGwr08qcqRgqU39EtRoEkwyaUykCqZmSiwnBub7DMFHNTx", - "RijWlvbFtJ8Aj4Tl33JjXWAsZEY7ljNhwkNzCMU4wTegjVAyDvkv7mMMdoq+R5rKMA+BmaoslbaQxdaA", - "2dQ4rldwW+NSyxbsUiurUpWjoCsD+yCPcakF3zPLrcQxiFufmdWZ43BxVARD37qNsrJDRMOIXYRchFEt", - "7raLNiOEYHhTzyTFEaanOXWlaDoxVpUl+iSbVLKeN8amCzf6zP7QjB0qF7eNr8wUIHYbaPKUbxxnXblu", - "zQ3zdLCCX6O/L7Va+Qh+SDMaY2KETCHZpflolhc4qm0Ce4x0ZC/2BwItbD3j6OlvVOlGlWCPFMYWfM/A", - "4LWrR102udpHCBBeguUiN3UQUBe9GixUH+ufXW64oYqptPkWdXgpdOFKzLR3mPCbCzEyj8UVUxuzlBnT", - "sOE6CyOGwZqvZMsMbuP+1pWwaQATcUKXNTZhWRqKvr5KPovvG1SndcSZWAWfPqA+FiLVirvCPDLe7Vm2", - "rj1rKDhSRyViv8eO4xRylbhzgMhu5b6Hc4JQn2mLKg43iGfU0GqJbNZApUf0nj0mtoW8ZKUGA2MLKZXK", - "E9Ba6ViVaeBn+piuRXoNGUOFpKjHu7/PujQhEvYAhWrqOtxmvQ0BVVmChOzhjLEzyciIfPze2+p6yOVn", - "dhf+W8KaVXQkwCWjRc6uZGzbCgcKH6hFAcxu3XEn7B+IygHZjcjeyhEF4huqhyG4qEbuzMovaGbLtw1c", - "eUupHBWHuM+v6diZd6QsMop2G/dlqkUh6Oy5NWyKviIcBwzTJWFnjF2StWC4auAGNM/pYM2EgoUwrBCY", - "9ZgqTQGy0yuZdChJVeERP2j+6wzxqjo6egLs6GF/jrEYp/jI3NlAf+5zdjR1n4hd7Dm7mlxNBpA0FOoG", - "MpedtPXazdoL9l9quFfy+4ErYgXfurwm2CIz1XIpUuGYniv0ZCvVCzekoi+gkTzA7MAwYafkvImjFKY5", - "uTQGGN8eP0YCHYGKARpuHlrzbSgCd3XHMLjlKa6Sk5PZsg0qSq1nw13OqjJpA4iWOHZg9MUnd9RhoTCt", - "0ux97a42K8JHf1M6t5u+y15C12FHS11n+4O2ATOiFBxi/mesVCh14Y97w5lgLowdEOkzS6o81goZ2XRm", - "7L9UxVJO9ltWFuqgXmmKlCmDQgy0iwacPjZpOAQ5FODybfry6FF/4Y8eeZkLw5awCT0SOLDPjkePnBEo", - "Y1+oohQ5fIQC8Zqb9VDSC27gyWN28c3Z0+PHf3v89HNcDMX7vGCLLW6sD3z9nhm7zeFhfHc0VW7j0D8/", - "CSfVXbh7S29EcA37EA25BPTajmPM9WUEPn6wJ+mZ+O15JPSidWJUEukPxNXM9q6Z4B601Bbo85cBITkl", - "Y2irvptOMGfNtx/BcTpATIOPFE2nemPcV7Vs97V4OzBbY6EYliDd1L+NxLBvQqo1iFiUzIWEpFASttFW", - "TiHhO/oYjXfI1EYmk9Mbm9tPRTv098jq4jlEmh/KX5J2SyVe1102H0H4fbi96nO7o4eidchLxlmaC6rs", - "KWmsrlJ7JTlVGnrhZE8tQv1kvPb0IgyJF7sitSgP6kpygzys6w+zmCdbQqSy+BVAKEGZarUC0wsv2RLg", - "SvpRQrJKCku4KDpPnMBK0OT4Zm4kRlRLnlOp7BfQii0q293CqPHARYiuFI5omFpeSW5ZDtxY9p2Ql7cE", - "LuSPQWck2I3S1zUX4vH/CiQYYZL43vC1+/oNN+uwfBwYnI2f7Kq9CL/pTtha6HQ2/veDP52+PUv+ypNf", - "jpJn/zp/9/7k7uGjwY+P754//5/uT0/unj/80+9jkgq0x47FPeXnL314d/6S9vCmCj6g/ZNVcQshk6iS", - "YdpVCEndVT3dYg8wEgkK9LCpp3upX0l7K1GRbnguMm5/nTr0XdzAFp119LSmI4heUS6s9V0sbVyppOTp", - "NZ3ZTVbCrqvFLFXFPIS185WqQ9x5xqFQkr5lc16KuSkhnd8c79kaP8BfsYi7osYTd7rfahyIhPf+qKiT", - "aSJE1zjtOm8w03oJSyEFfj+9khm3fL7gRqRmXhnQX/CcyxRmK8VOmQf5kltOBYpeXW3sbgO1hXpqymqR", - "i5Rdt/e3Rt/H6lRXV2+R61dX7wbHPMPdyKOKKr5DkGyEXavKJr42OV7kaApBBNmVyXZhnTIP24nZ1z49", - "/Lj/o5qhiS8aP+Gq3RhUk6aAH4oqKMNXyh9mab4JHZuVAcN+LHj5Vkj7jiW+AECt99+oHAn70dsoOtZt", - "CZ1cb2dXSQtGLL3jlV0nqA/RVRlkC8mydX+Er9A4wqkK5qPION/PvACWriG9hoxKx1R8m3amh8NM72qC", - "ugnjWpBdHwj1yVGetQBWlRn3zpjLbb9hyYC1oUvrDVzD9lI1bXb36VC6m058fTjZJeiSa+RIyy+oZZB6", - "qC+PCf60lnxY9i7Rf5DMY8IuubYiFSW3PlY6oB/pdWcOAtlniVHbwzSxa2LOHFtMipqcG5xgZhgVB+AX", - "lAcqT//APGByuTp3Bxp0f8sHtIscWpV541Waa3L7YdnuQsoYaXEtAS0bFxjI6HKk7WvX/kRF3DTnKHSS", - "dohX2lvYRy0KR6CiW9AUiDeHGz5aWx5tnDxvnWu2+vHrtshg0X1jmNYtsu5qXGifDD2ToVFyMr1X0+N0", - "4ttXYuJQMkdxZJDDivtSKjXGeEXxpH1mWgJCOr5fLjFRY0nsiJQbo1LhzpMaJ+ZxAO7YjxhzKSY7GEJM", - "jVtkUw2KALNXqm2bcnUfIiUIKlrxAJuqV62/YX/tobmj6GOBvXv20Hc0RjRteoidGId58HQSdUlj4VRn", - "FHNDFjAI6mIqiq5pmBkO808DOdA+lHQ8a3IdqxfgdgqkhhdhWivGYg/EEne3h61SpIYVZiFN5I7WGlLR", - "T5s93SgLyVJoYxNKGqLLw0FfGYqCvsKhcffTYRVzl5xEFvc+hPYatkkm8ioubY/3zy8R7as62DTV4hq2", - "tMkAT9dsQZfycBfqoMcxO1C7NoGdC/7WLfhb/tHWe5gu4VBErBXmeh0cvxGt6vmTXcYUUcCYcgylNsrS", - "qHuhuGnHVY+F8lepKyl+roCJDKTFT9qfUXU8C3I3NBoMXMdIU4MH7PsaavDxk3bKVg8KBl1iO2C5I6KG", - "NMqTkD9EOkiCVw0LrRMf/KEV/d4jdW1jHGSuO9JOtIYm23RFtXU3D2jffB4mApWQ1t2S2X/tOuzNa0fo", - "CI7oNWpKEmLtEeHghDbvkEq4fYmaVeoW+XY6Fbo2BqrXTAx5FLXCuONUnhsVAVPJDZfuViTOczz0sw24", - "jRFnbZSmRkcD0WKYMMlSq18g7q6XKKjIsZlnJR140exZpIGsH4TUoUdz3z3wt03HqGq/ro0oImdfD+qW", - "FkYsnLS8lR9SH0CI4rh0au1ucHaqRHHjaFd25w5+Yxye5kE1POebBY/d6ri6epsiTWdNCt6JN61iYXKQ", - "gqnbX7zutbL5eqxw3YEl6OZse9jdPabuly31+82rfAapKHgeTz8y4n63PzwTK+GuwVYGWvcsPSD3foDT", - "In9X1RU5GtacL9nRtHWT20sjEzfCiEUONOLYjcAsmdZWZzxhCi4PpF0bGv74gOHrSmYaMrs2jrFGMcyr", - "L+sL63WCtwC7AZDsiMYdP2MPKLU14gYeIhcLdzt4cnr8jKq/7o+j2Gbn77vv8isZOZb/8I4lrseU2zsY", - "uEl5qLNop6p7pGTche2wJjf1EFuikd7r7belgku+gnitrthDk5tL0qTIuMcXmbkb9sZqtWXCxvGD5eif", - "Rk6A0P05Mnx7U4EGZBUzqkB9ai5ROqQBnLuu7294BbrCR6ojlKFNrXUS+emzILeXx1ZN1Z5XvIAuW6eY", - "ytN5rGiuoXiHOBvpjAB9E0eiRwQc9k0/lz2QSiYF2k72sDlbbOlfDDFVqqJobfBd/Xr+btCHhloIJRll", - "bNVhLG/5pF/N4krH18krRPXDm2/9xlAoHbsr1XhDv0losFrATdRi+2dkdWRSbxeB87EA5UutlW6fyA+6", - "wlwzXn1Jjd7SUOGSJRlPXQnvxgr4LXLrHS28vhe3ey3jN9ymk7+M3gRxhx/csg0wLqWy3EIQJuOsUBnk", - "zPjGwBxWPN36ozZzJZHhmdBA3XWioBsJnJkNX61A0xmtpvghHPUTtOHaF5XIs31pk4fxBY2NHH3/Iw+v", - "h9UZR6xLLHsdgC0T75wG9C++0kJ3H9bWaP5eB7S4abhDhg77o8eU9fkRgmBEfnOLprHaiPg1l+k6yiGC", - "0nqhINJOv+ZSQh6d7ba8f5CGFPwnNUJzIWT8U18FHGN6bGjW3F1hQBngR/qWphMDaaWF3V6gVfkMvhR/", - "i9a0vq7t118/r4N7H1u6Bz+8122svXmj4Wvl+vkKDGao7G6pZfPLW16UOfjg9Plniz/Akz+eZEdPjv+w", - "+OPR06MUTp4+Ozriz0748bMnx/D4j09PjuB4+fmzxePs8cnjxcnjk8+fPkufnBwvTj5/9ofPwgMJjtDm", - "8YH/pN6d5Oz1eXKJxDaC4qX4M2xd+wFqZ+iv4ikVM6DgIp+chp/+LdgJGlDrTTf/68RvYpO1taU5nc83", - "m82sPWW+omsziVVVup4HPMMO2dfnDGTmMg3KZcmW0FjIdlxlVNicChj07c2XF5fs7PX5rHEHk9PJ0exo", - "dkztdiVIXorJ6eQJ/URavya5z9fAc4uWcTedzAvcNFPj//IufOZby/Cnm8fzcCo3f+8ztrtd37opsz9o", - "aCa4O6bz91TpbAHyl8Tm75tbm3dON3OwkU03XCpohtNlAbpLb9yvqI4hYBWme3O25u15hjzFWS/qG6zt", - "lzLf/j99V+5d77WNx0dH/3w5ga4AntyTE7uim25kGcH7Bc/YG/i5AmMd7uNPh/tcUu0e3QxzbvRuOnn6", - "KVd/LtEUeM5oZKv8MFSJH+S1VBsZRuKeVxUF19tg3qbjLMJ9dfKsfGXocpMWN9zC5B3dnjP2YKdDT1Tc", - "2+nQuxv/dDqfyun8th8k+afT+a05nQvnFA53Oj4QyiFbgZ67ywNNfBROi4dHqN24bMxz+TCdPaA6hYTN", - "Q3/z24GNHMcz1dzAclGp74MN9S6PdTbwbG880E7nx59ha/a5ucs1sB+bl8V/pKo8NR1OmdLsR57nrd/o", - "gcgQgM5Gnimvm2MOfaP87m4aI2sJEM4I6CzA3yNEd38N4TDf8aBzHWDGXjrtMfUF3/oqwhJGnyp1Hdtt", - "z+ZV8Pjo6CjWA96n2eVYnmI6k9moJIcbyIeiHiOid6a/62G/0UdYhq0Y7ZwxonXhHdy6O2P0ncNuf8F9", - "qHup5GeWbbjwF/hbjazuqZtC2PAEqLvm6uu69d4RfzYyQZC7X5X90C3ut3ef7W6HszPrymZqI8cdFzV9", - "8NyfmtA5Rp0qW8UCgNpTzVh43C7fhkdJGadHz1Vlu28Fhza93vXnuoN6JSQhICsnLO54kLeK7/4VlKET", - "vPCUvXKPxvT8XvTJREdj3O5jRv+hunR4ALJThqHds/P3HE0Bgz33AlVCnBum/RZ4PvdXP1u/dm85R36d", - "10030Y/9qkPs6/y9vRWOllaFjKRT18bevkMm03GOF1xT8Dmdz3OV8nytjJ1P0Ml0i0Htj+9q/r0P0g58", - "vHt3978BAAD//9RHW+GqYwAA", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index 00cfa6da51..ea3721b651 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -4,6 +4,7 @@ package private import ( + "encoding/json" "time" ) @@ -19,6 +20,14 @@ type Account struct { // specifies the amount of MicroAlgos in the account, without the pending rewards. AmountWithoutPendingRewards uint64 `json:"amount-without-pending-rewards"` + // \[appl\] applications local data stored in this account. + // + // Note the raw object uses `map[int] -> AppLocalState` for this type. + AppsLocalState *[]ApplicationLocalStates `json:"apps-local-state,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + AppsTotalSchema *ApplicationStateSchema `json:"apps-total-schema,omitempty"` + // \[asset\] assets held by this account. // // Note the raw object uses `map[int] -> AssetHolding` for this type. @@ -27,6 +36,11 @@ type Account struct { // \[spend\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field. AuthAddr *string `json:"auth-addr,omitempty"` + // \[appp\] parameters of applications created by this account including app global data. + // + // Note: the raw account uses `map[int] -> AppParams` for this type. + CreatedApps *[]Application `json:"created-apps,omitempty"` + // \[apar\] parameters of assets created by this account. // // Note: the raw account uses `map[int] -> Asset` for this type. @@ -79,6 +93,74 @@ type AccountParticipation struct { VoteParticipationKey []byte `json:"vote-participation-key"` } +// AccountStateDelta defines model for AccountStateDelta. +type AccountStateDelta struct { + Address string `json:"address"` + + // Application state delta. + Delta StateDelta `json:"delta"` +} + +// Application defines model for Application. +type Application struct { + + // \[appidx\] application index. + Id uint64 `json:"id"` + + // Stores the global information associated with an application. + Params ApplicationParams `json:"params"` +} + +// ApplicationLocalState defines model for ApplicationLocalState. +type ApplicationLocalState struct { + + // Represents a key-value store for use in an application. + KeyValue TealKeyValueStore `json:"key-value"` + + // Specifies maximums on the number of each type that may be stored. + Schema ApplicationStateSchema `json:"schema"` +} + +// ApplicationLocalStates defines model for ApplicationLocalStates. +type ApplicationLocalStates struct { + Id uint64 `json:"id"` + + // Stores local state associated with an application. + State ApplicationLocalState `json:"state"` +} + +// ApplicationParams defines model for ApplicationParams. +type ApplicationParams struct { + + // \[approv\] approval program. + ApprovalProgram []byte `json:"approval-program"` + + // \[clearp\] approval program. + ClearStateProgram []byte `json:"clear-state-program"` + + // The address that created this application. This is the address where the parameters and global state for this application can be found. + Creator string `json:"creator"` + + // Represents a key-value store for use in an application. + GlobalState *TealKeyValueStore `json:"global-state,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + GlobalStateSchema *ApplicationStateSchema `json:"global-state-schema,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + LocalStateSchema *ApplicationStateSchema `json:"local-state-schema,omitempty"` +} + +// ApplicationStateSchema defines model for ApplicationStateSchema. +type ApplicationStateSchema struct { + + // \[nbs\] num of byte slices. + NumByteSlice uint64 `json:"num-byte-slice"` + + // \[nui\] num of uints. + NumUint uint64 `json:"num-uint"` +} + // Asset defines model for Asset. type Asset struct { @@ -150,12 +232,117 @@ type AssetParams struct { Url *string `json:"url,omitempty"` } +// DryrunRequest defines model for DryrunRequest. +type DryrunRequest struct { + Accounts []Account `json:"accounts"` + Apps []Application `json:"apps"` + + // LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to. + LatestTimestamp uint64 `json:"latest-timestamp"` + + // ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in. + ProtocolVersion string `json:"protocol-version"` + + // Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to. + Round uint64 `json:"round"` + Sources []DryrunSource `json:"sources"` + Txns []json.RawMessage `json:"txns"` +} + +// DryrunSource defines model for DryrunSource. +type DryrunSource struct { + AppIndex uint64 `json:"app-index"` + + // FieldName is what kind of sources this is. If lsig then it goes into the transactions[this.TxnIndex].LogicSig. If approv or clearp it goes into the Approval Program or Clear State Program of application[this.AppIndex]. + FieldName string `json:"field-name"` + Source string `json:"source"` + TxnIndex uint64 `json:"txn-index"` +} + +// DryrunState defines model for DryrunState. +type DryrunState struct { + + // Evaluation error if any + Error *string `json:"error,omitempty"` + + // Line number + Line uint64 `json:"line"` + + // Program counter + Pc uint64 `json:"pc"` + Scratch *[]TealValue `json:"scratch,omitempty"` + Stack []TealValue `json:"stack"` +} + +// DryrunTxnResult defines model for DryrunTxnResult. +type DryrunTxnResult struct { + AppCallMessages *[]string `json:"app-call-messages,omitempty"` + AppCallTrace *[]DryrunState `json:"app-call-trace,omitempty"` + + // Disassembled program line by line. + Disassembly []string `json:"disassembly"` + + // Application state delta. + GlobalDelta *StateDelta `json:"global-delta,omitempty"` + LocalDeltas *[]AccountStateDelta `json:"local-deltas,omitempty"` + LogicSigMessages *[]string `json:"logic-sig-messages,omitempty"` + LogicSigTrace *[]DryrunState `json:"logic-sig-trace,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *string `json:"data,omitempty"` Message string `json:"message"` } +// EvalDelta defines model for EvalDelta. +type EvalDelta struct { + + // \[at\] delta action. + Action uint64 `json:"action"` + + // \[bs\] bytes value. + Bytes *string `json:"bytes,omitempty"` + + // \[ui\] uint value. + Uint *uint64 `json:"uint,omitempty"` +} + +// EvalDeltaKeyValue defines model for EvalDeltaKeyValue. +type EvalDeltaKeyValue struct { + Key string `json:"key"` + + // Represents a TEAL value delta. + Value EvalDelta `json:"value"` +} + +// StateDelta defines model for StateDelta. +type StateDelta []EvalDeltaKeyValue + +// TealKeyValue defines model for TealKeyValue. +type TealKeyValue struct { + Key string `json:"key"` + + // Represents a TEAL value. + Value TealValue `json:"value"` +} + +// TealKeyValueStore defines model for TealKeyValueStore. +type TealKeyValueStore []TealKeyValue + +// TealValue defines model for TealValue. +type TealValue struct { + + // \[tb\] bytes value. + Bytes string `json:"bytes"` + + // \[tt\] value type. + Type uint64 `json:"type"` + + // \[ui\] uint value. + Uint uint64 `json:"uint"` +} + // Version defines model for Version. type Version struct { @@ -245,6 +432,12 @@ type TxType string // AccountResponse defines model for AccountResponse. type AccountResponse Account +// ApplicationResponse defines model for ApplicationResponse. +type ApplicationResponse Application + +// AssetResponse defines model for AssetResponse. +type AssetResponse Asset + // BlockResponse defines model for BlockResponse. type BlockResponse struct { @@ -269,6 +462,25 @@ type CatchpointStartResponse struct { CatchupMessage string `json:"catchup-message"` } +// CompileResponse defines model for CompileResponse. +type CompileResponse struct { + + // base32 SHA512_256 of program bytes (Address style) + Hash string `json:"hash"` + + // base64 encoded program bytes + Result string `json:"result"` +} + +// DryrunResponse defines model for DryrunResponse. +type DryrunResponse struct { + Error string `json:"error"` + + // Protocol version is the protocol version Dryrun was operated under. + ProtocolVersion string `json:"protocol-version"` + Txns []DryrunTxnResult `json:"txns"` +} + // NodeStatusResponse defines model for NodeStatusResponse. type NodeStatusResponse struct { @@ -353,16 +565,6 @@ type PendingTransactionsResponse struct { TotalTransactions uint64 `json:"total-transactions"` } -// PostCompileResponse defines model for PostCompileResponse. -type PostCompileResponse struct { - - // base32 SHA512_256 of program bytes (Address style) - Hash string `json:"hash"` - - // base64 encoded program bytes - Result string `json:"result"` -} - // PostTransactionsResponse defines model for PostTransactionsResponse. type PostTransactionsResponse struct { diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 5bf9908564..bc2cf5402e 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -19,10 +19,16 @@ import ( type ServerInterface interface { // Get account information. // (GET /v2/accounts/{address}) - AccountInformation(ctx echo.Context, address string) error + AccountInformation(ctx echo.Context, address string, params AccountInformationParams) error // Get a list of unconfirmed transactions currently in the transaction pool by address. // (GET /v2/accounts/{address}/transactions/pending) GetPendingTransactionsByAddress(ctx echo.Context, address string, params GetPendingTransactionsByAddressParams) error + // Get application information. + // (GET /v2/applications/{application-id}) + GetApplicationByID(ctx echo.Context, applicationId uint64) error + // Get asset information. + // (GET /v2/assets/{asset-id}) + GetAssetByID(ctx echo.Context, assetId uint64) error // Get the block for the given round. // (GET /v2/blocks/{round}) GetBlock(ctx echo.Context, round uint64, params GetBlockParams) error @@ -38,6 +44,9 @@ type ServerInterface interface { // Compile TEAL source code to binary, produce its hash // (POST /v2/teal/compile) TealCompile(ctx echo.Context) error + // Provide debugging information for a transaction (or group). + // (POST /v2/teal/dryrun) + TealDryrun(ctx echo.Context) error // Broadcasts a raw transaction to the network. // (POST /v2/transactions) RawTransaction(ctx echo.Context) error @@ -62,6 +71,7 @@ func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { validQueryParams := map[string]bool{ "pretty": true, + "format": true, } // Check for unknown query parameters. @@ -82,8 +92,20 @@ func (w *ServerInterfaceWrapper) AccountInformation(ctx echo.Context) error { ctx.Set("api_key.Scopes", []string{""}) + // Parameter object where we will unmarshal all parameters from the context + var params AccountInformationParams + // ------------- Optional query parameter "format" ------------- + if paramValue := ctx.QueryParam("format"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "format", ctx.QueryParams(), ¶ms.Format) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter format: %s", err)) + } + // Invoke the callback with all the unmarshalled arguments - err = w.Handler.AccountInformation(ctx, address) + err = w.Handler.AccountInformation(ctx, address, params) return err } @@ -141,6 +163,66 @@ func (w *ServerInterfaceWrapper) GetPendingTransactionsByAddress(ctx echo.Contex return err } +// GetApplicationByID converts echo context to params. +func (w *ServerInterfaceWrapper) GetApplicationByID(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "application-id" ------------- + var applicationId uint64 + + err = runtime.BindStyledParameter("simple", false, "application-id", ctx.Param("application-id"), &applicationId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter application-id: %s", err)) + } + + ctx.Set("api_key.Scopes", []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetApplicationByID(ctx, applicationId) + return err +} + +// GetAssetByID converts echo context to params. +func (w *ServerInterfaceWrapper) GetAssetByID(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "asset-id" ------------- + var assetId uint64 + + err = runtime.BindStyledParameter("simple", false, "asset-id", ctx.Param("asset-id"), &assetId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter asset-id: %s", err)) + } + + ctx.Set("api_key.Scopes", []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetAssetByID(ctx, assetId) + return err +} + // GetBlock converts echo context to params. func (w *ServerInterfaceWrapper) GetBlock(ctx echo.Context) error { @@ -283,6 +365,29 @@ func (w *ServerInterfaceWrapper) TealCompile(ctx echo.Context) error { return err } +// TealDryrun converts echo context to params. +func (w *ServerInterfaceWrapper) TealDryrun(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + + ctx.Set("api_key.Scopes", []string{""}) + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.TealDryrun(ctx) + return err +} + // RawTransaction converts echo context to params. func (w *ServerInterfaceWrapper) RawTransaction(ctx echo.Context) error { @@ -438,11 +543,14 @@ func RegisterHandlers(router interface { router.GET("/v2/accounts/:address", wrapper.AccountInformation, m...) router.GET("/v2/accounts/:address/transactions/pending", wrapper.GetPendingTransactionsByAddress, m...) + router.GET("/v2/applications/:application-id", wrapper.GetApplicationByID, m...) + router.GET("/v2/assets/:asset-id", wrapper.GetAssetByID, m...) router.GET("/v2/blocks/:round", wrapper.GetBlock, m...) router.GET("/v2/ledger/supply", wrapper.GetSupply, m...) router.GET("/v2/status", wrapper.GetStatus, m...) router.GET("/v2/status/wait-for-block-after/:round", wrapper.WaitForBlock, m...) router.POST("/v2/teal/compile", wrapper.TealCompile, m...) + router.POST("/v2/teal/dryrun", wrapper.TealDryrun, m...) router.POST("/v2/transactions", wrapper.RawTransaction, m...) router.GET("/v2/transactions/params", wrapper.TransactionParams, m...) router.GET("/v2/transactions/pending", wrapper.GetPendingTransactions, m...) @@ -453,120 +561,160 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "/19yhfQAqAAA", + "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", + "AAAA///QLTg0r9cAAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index 228233b7f7..dec25467c9 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -4,6 +4,7 @@ package generated import ( + "encoding/json" "time" ) @@ -19,6 +20,14 @@ type Account struct { // specifies the amount of MicroAlgos in the account, without the pending rewards. AmountWithoutPendingRewards uint64 `json:"amount-without-pending-rewards"` + // \[appl\] applications local data stored in this account. + // + // Note the raw object uses `map[int] -> AppLocalState` for this type. + AppsLocalState *[]ApplicationLocalStates `json:"apps-local-state,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + AppsTotalSchema *ApplicationStateSchema `json:"apps-total-schema,omitempty"` + // \[asset\] assets held by this account. // // Note the raw object uses `map[int] -> AssetHolding` for this type. @@ -27,6 +36,11 @@ type Account struct { // \[spend\] the address against which signing should be checked. If empty, the address of the current account is used. This field can be updated in any transaction by setting the RekeyTo field. AuthAddr *string `json:"auth-addr,omitempty"` + // \[appp\] parameters of applications created by this account including app global data. + // + // Note: the raw account uses `map[int] -> AppParams` for this type. + CreatedApps *[]Application `json:"created-apps,omitempty"` + // \[apar\] parameters of assets created by this account. // // Note: the raw account uses `map[int] -> Asset` for this type. @@ -79,6 +93,74 @@ type AccountParticipation struct { VoteParticipationKey []byte `json:"vote-participation-key"` } +// AccountStateDelta defines model for AccountStateDelta. +type AccountStateDelta struct { + Address string `json:"address"` + + // Application state delta. + Delta StateDelta `json:"delta"` +} + +// Application defines model for Application. +type Application struct { + + // \[appidx\] application index. + Id uint64 `json:"id"` + + // Stores the global information associated with an application. + Params ApplicationParams `json:"params"` +} + +// ApplicationLocalState defines model for ApplicationLocalState. +type ApplicationLocalState struct { + + // Represents a key-value store for use in an application. + KeyValue TealKeyValueStore `json:"key-value"` + + // Specifies maximums on the number of each type that may be stored. + Schema ApplicationStateSchema `json:"schema"` +} + +// ApplicationLocalStates defines model for ApplicationLocalStates. +type ApplicationLocalStates struct { + Id uint64 `json:"id"` + + // Stores local state associated with an application. + State ApplicationLocalState `json:"state"` +} + +// ApplicationParams defines model for ApplicationParams. +type ApplicationParams struct { + + // \[approv\] approval program. + ApprovalProgram []byte `json:"approval-program"` + + // \[clearp\] approval program. + ClearStateProgram []byte `json:"clear-state-program"` + + // The address that created this application. This is the address where the parameters and global state for this application can be found. + Creator string `json:"creator"` + + // Represents a key-value store for use in an application. + GlobalState *TealKeyValueStore `json:"global-state,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + GlobalStateSchema *ApplicationStateSchema `json:"global-state-schema,omitempty"` + + // Specifies maximums on the number of each type that may be stored. + LocalStateSchema *ApplicationStateSchema `json:"local-state-schema,omitempty"` +} + +// ApplicationStateSchema defines model for ApplicationStateSchema. +type ApplicationStateSchema struct { + + // \[nbs\] num of byte slices. + NumByteSlice uint64 `json:"num-byte-slice"` + + // \[nui\] num of uints. + NumUint uint64 `json:"num-uint"` +} + // Asset defines model for Asset. type Asset struct { @@ -150,12 +232,117 @@ type AssetParams struct { Url *string `json:"url,omitempty"` } +// DryrunRequest defines model for DryrunRequest. +type DryrunRequest struct { + Accounts []Account `json:"accounts"` + Apps []Application `json:"apps"` + + // LatestTimestamp is available to some TEAL scripts. Defaults to the latest confirmed timestamp this algod is attached to. + LatestTimestamp uint64 `json:"latest-timestamp"` + + // ProtocolVersion specifies a specific version string to operate under, otherwise whatever the current protocol of the network this algod is running in. + ProtocolVersion string `json:"protocol-version"` + + // Round is available to some TEAL scripts. Defaults to the current round on the network this algod is attached to. + Round uint64 `json:"round"` + Sources []DryrunSource `json:"sources"` + Txns []json.RawMessage `json:"txns"` +} + +// DryrunSource defines model for DryrunSource. +type DryrunSource struct { + AppIndex uint64 `json:"app-index"` + + // FieldName is what kind of sources this is. If lsig then it goes into the transactions[this.TxnIndex].LogicSig. If approv or clearp it goes into the Approval Program or Clear State Program of application[this.AppIndex]. + FieldName string `json:"field-name"` + Source string `json:"source"` + TxnIndex uint64 `json:"txn-index"` +} + +// DryrunState defines model for DryrunState. +type DryrunState struct { + + // Evaluation error if any + Error *string `json:"error,omitempty"` + + // Line number + Line uint64 `json:"line"` + + // Program counter + Pc uint64 `json:"pc"` + Scratch *[]TealValue `json:"scratch,omitempty"` + Stack []TealValue `json:"stack"` +} + +// DryrunTxnResult defines model for DryrunTxnResult. +type DryrunTxnResult struct { + AppCallMessages *[]string `json:"app-call-messages,omitempty"` + AppCallTrace *[]DryrunState `json:"app-call-trace,omitempty"` + + // Disassembled program line by line. + Disassembly []string `json:"disassembly"` + + // Application state delta. + GlobalDelta *StateDelta `json:"global-delta,omitempty"` + LocalDeltas *[]AccountStateDelta `json:"local-deltas,omitempty"` + LogicSigMessages *[]string `json:"logic-sig-messages,omitempty"` + LogicSigTrace *[]DryrunState `json:"logic-sig-trace,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *string `json:"data,omitempty"` Message string `json:"message"` } +// EvalDelta defines model for EvalDelta. +type EvalDelta struct { + + // \[at\] delta action. + Action uint64 `json:"action"` + + // \[bs\] bytes value. + Bytes *string `json:"bytes,omitempty"` + + // \[ui\] uint value. + Uint *uint64 `json:"uint,omitempty"` +} + +// EvalDeltaKeyValue defines model for EvalDeltaKeyValue. +type EvalDeltaKeyValue struct { + Key string `json:"key"` + + // Represents a TEAL value delta. + Value EvalDelta `json:"value"` +} + +// StateDelta defines model for StateDelta. +type StateDelta []EvalDeltaKeyValue + +// TealKeyValue defines model for TealKeyValue. +type TealKeyValue struct { + Key string `json:"key"` + + // Represents a TEAL value. + Value TealValue `json:"value"` +} + +// TealKeyValueStore defines model for TealKeyValueStore. +type TealKeyValueStore []TealKeyValue + +// TealValue defines model for TealValue. +type TealValue struct { + + // \[tb\] bytes value. + Bytes string `json:"bytes"` + + // \[tt\] value type. + Type uint64 `json:"type"` + + // \[ui\] uint value. + Uint uint64 `json:"uint"` +} + // Version defines model for Version. type Version struct { @@ -245,6 +432,12 @@ type TxType string // AccountResponse defines model for AccountResponse. type AccountResponse Account +// ApplicationResponse defines model for ApplicationResponse. +type ApplicationResponse Application + +// AssetResponse defines model for AssetResponse. +type AssetResponse Asset + // BlockResponse defines model for BlockResponse. type BlockResponse struct { @@ -269,6 +462,25 @@ type CatchpointStartResponse struct { CatchupMessage string `json:"catchup-message"` } +// CompileResponse defines model for CompileResponse. +type CompileResponse struct { + + // base32 SHA512_256 of program bytes (Address style) + Hash string `json:"hash"` + + // base64 encoded program bytes + Result string `json:"result"` +} + +// DryrunResponse defines model for DryrunResponse. +type DryrunResponse struct { + Error string `json:"error"` + + // Protocol version is the protocol version Dryrun was operated under. + ProtocolVersion string `json:"protocol-version"` + Txns []DryrunTxnResult `json:"txns"` +} + // NodeStatusResponse defines model for NodeStatusResponse. type NodeStatusResponse struct { @@ -353,16 +565,6 @@ type PendingTransactionsResponse struct { TotalTransactions uint64 `json:"total-transactions"` } -// PostCompileResponse defines model for PostCompileResponse. -type PostCompileResponse struct { - - // base32 SHA512_256 of program bytes (Address style) - Hash string `json:"hash"` - - // base64 encoded program bytes - Result string `json:"result"` -} - // PostTransactionsResponse defines model for PostTransactionsResponse. type PostTransactionsResponse struct { @@ -410,6 +612,13 @@ type TransactionParametersResponse struct { MinFee uint64 `json:"min-fee"` } +// AccountInformationParams defines parameters for AccountInformation. +type AccountInformationParams struct { + + // Configures whether the response object is JSON or MessagePack encoded. + Format *string `json:"format,omitempty"` +} + // GetPendingTransactionsByAddressParams defines parameters for GetPendingTransactionsByAddress. type GetPendingTransactionsByAddressParams struct { @@ -427,6 +636,9 @@ type GetBlockParams struct { Format *string `json:"format,omitempty"` } +// TealDryrunJSONBody defines parameters for TealDryrun. +type TealDryrunJSONBody DryrunRequest + // GetPendingTransactionsParams defines parameters for GetPendingTransactions. type GetPendingTransactionsParams struct { @@ -443,3 +655,6 @@ type PendingTransactionInformationParams struct { // Configures whether the response object is JSON or MessagePack encoded. Format *string `json:"format,omitempty"` } + +// TealDryrunRequestBody defines body for TealDryrun for application/json ContentType. +type TealDryrunJSONRequestBody TealDryrunJSONBody diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index a0f251dd8a..09a6bb5d06 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -44,6 +44,7 @@ import ( ) const maxTealSourceBytes = 1e5 +const maxTealDryrunBytes = 1e5 // Handlers is an implementation to the V2 route handler interface defined by the generated code. type Handlers struct { @@ -83,7 +84,12 @@ func (v2 *Handlers) ShutdownNode(ctx echo.Context, params private.ShutdownNodePa // AccountInformation gets account information for a given account. // (GET /v2/accounts/{address}) -func (v2 *Handlers) AccountInformation(ctx echo.Context, address string) error { +func (v2 *Handlers) AccountInformation(ctx echo.Context, address string, params generated.AccountInformationParams) error { + handle, contentType, err := getCodecHandle(params.Format) + if err != nil { + return badRequest(ctx, err, errFailedParsingFormatOption, v2.Log) + } + addr, err := basics.UnmarshalChecksumAddress(address) if err != nil { return badRequest(ctx, err, errFailedToParseAddress, v2.Log) @@ -91,99 +97,48 @@ func (v2 *Handlers) AccountInformation(ctx echo.Context, address string) error { myLedger := v2.Node.Ledger() lastRound := myLedger.Latest() - record, err := myLedger.Lookup(lastRound, basics.Address(addr)) + record, err := myLedger.Lookup(lastRound, addr) if err != nil { return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) } - recordWithoutPendingRewards, err := myLedger.LookupWithoutRewards(lastRound, basics.Address(addr)) + + if handle == protocol.CodecHandle { + data, err := encode(handle, record) + if err != nil { + return internalError(ctx, err, errFailedToEncodeResponse, v2.Log) + } + return ctx.Blob(http.StatusOK, contentType, data) + } + + recordWithoutPendingRewards, err := myLedger.LookupWithoutRewards(lastRound, addr) if err != nil { return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) } - - amount := record.MicroAlgos amountWithoutPendingRewards := recordWithoutPendingRewards.MicroAlgos - pendingRewards, overflowed := basics.OSubA(amount, amountWithoutPendingRewards) - if overflowed { - return internalError(ctx, err, errInternalFailure, v2.Log) - } - assets := make([]generated.AssetHolding, 0) + assetsCreators := make(map[basics.AssetIndex]string, len(record.Assets)) if len(record.Assets) > 0 { //assets = make(map[uint64]v1.AssetHolding) - for curid, holding := range record.Assets { + for curid := range record.Assets { var creator string - creatorAddr, err := myLedger.GetAssetCreator(curid) - if err == nil { + creatorAddr, ok, err := myLedger.GetCreator(basics.CreatableIndex(curid), basics.AssetCreatable) + if err == nil && ok { creator = creatorAddr.String() } else { // Asset may have been deleted, so we can no // longer fetch the creator creator = "" } - - holding := generated.AssetHolding{ - Amount: holding.Amount, - AssetId: uint64(curid), - Creator: creator, - IsFrozen: holding.Frozen, - } - - assets = append(assets, holding) - } - } - - createdAssets := make([]generated.Asset, 0) - if len(record.AssetParams) > 0 { - for idx, params := range record.AssetParams { - assetParams := generated.AssetParams{ - Creator: address, - Total: params.Total, - Decimals: uint64(params.Decimals), - DefaultFrozen: ¶ms.DefaultFrozen, - MetadataHash: byteOrNil(params.MetadataHash[:]), - Name: strOrNil(params.AssetName), - UnitName: strOrNil(params.UnitName), - Url: strOrNil(params.URL), - Clawback: addrOrNil(params.Clawback), - Freeze: addrOrNil(params.Freeze), - Manager: addrOrNil(params.Manager), - Reserve: addrOrNil(params.Reserve), - } - asset := generated.Asset{ - Index: uint64(idx), - Params: assetParams, - } - createdAssets = append(createdAssets, asset) + assetsCreators[curid] = creator } } - var apiParticipation *generated.AccountParticipation - if record.VoteID != (crypto.OneTimeSignatureVerifier{}) { - apiParticipation = &generated.AccountParticipation{ - VoteParticipationKey: record.VoteID[:], - SelectionParticipationKey: record.SelectionID[:], - VoteFirstValid: uint64(record.VoteFirstValid), - VoteLastValid: uint64(record.VoteLastValid), - VoteKeyDilution: uint64(record.VoteKeyDilution), - } - } - - response := generated.AccountResponse{ - SigType: nil, - Round: uint64(lastRound), - Address: addr.String(), - Amount: amount.Raw, - PendingRewards: pendingRewards.Raw, - AmountWithoutPendingRewards: amountWithoutPendingRewards.Raw, - Rewards: record.RewardedMicroAlgos.Raw, - Status: record.Status.String(), - RewardBase: &record.RewardsBase, - Participation: apiParticipation, - CreatedAssets: &createdAssets, - Assets: &assets, - AuthAddr: addrOrNil(record.AuthAddr), + account, err := AccountDataToAccount(address, &record, assetsCreators, lastRound, amountWithoutPendingRewards) + if err != nil { + return internalError(ctx, err, errInternalFailure, v2.Log) } + response := generated.AccountResponse(account) return ctx.JSON(http.StatusOK, response) } @@ -364,6 +319,72 @@ func (v2 *Handlers) RawTransaction(ctx echo.Context) error { return ctx.JSON(http.StatusOK, generated.PostTransactionsResponse{TxId: txid.String()}) } +// TealDryrun takes transactions and additional simulated ledger state and returns debugging information. +// (POST /v2/teal/dryrun) +func (v2 *Handlers) TealDryrun(ctx echo.Context) error { + if !v2.Node.Config().EnableDeveloperAPI { + return ctx.String(http.StatusNotFound, "/teal/dryrun was not enabled in the configuration file by setting the EnableDeveloperAPI to true") + } + req := ctx.Request() + buf := new(bytes.Buffer) + req.Body = http.MaxBytesReader(nil, req.Body, maxTealDryrunBytes) + buf.ReadFrom(req.Body) + data := buf.Bytes() + + var dr DryrunRequest + var gdr generated.DryrunRequest + err := decode(protocol.JSONHandle, data, &gdr) + if err == nil { + dr, err = DryrunRequestFromGenerated(&gdr) + if err != nil { + return badRequest(ctx, err, err.Error(), v2.Log) + } + } else { + err = decode(protocol.CodecHandle, data, &dr) + if err != nil { + return badRequest(ctx, err, err.Error(), v2.Log) + } + } + + // fetch previous block header just once to prevent racing with network + var hdr bookkeeping.BlockHeader + if dr.ProtocolVersion == "" || dr.Round == 0 || dr.LatestTimestamp == 0 { + actualLedger := v2.Node.Ledger() + hdr, err = actualLedger.BlockHdr(actualLedger.Latest()) + if err != nil { + return internalError(ctx, err, "current block error", v2.Log) + } + } + + var response generated.DryrunResponse + + var proto config.ConsensusParams + var protocolVersion protocol.ConsensusVersion + if dr.ProtocolVersion != "" { + var ok bool + proto, ok = config.Consensus[protocol.ConsensusVersion(dr.ProtocolVersion)] + if !ok { + return badRequest(ctx, nil, "unsupported protocol version", v2.Log) + } + protocolVersion = protocol.ConsensusVersion(dr.ProtocolVersion) + } else { + proto = config.Consensus[hdr.CurrentProtocol] + protocolVersion = hdr.CurrentProtocol + } + + if dr.Round == 0 { + dr.Round = uint64(hdr.Round + 1) + } + + if dr.LatestTimestamp == 0 { + dr.LatestTimestamp = hdr.TimeStamp + } + + doDryrunRequest(&dr, &proto, &response) + response.ProtocolVersion = string(protocolVersion) + return ctx.JSON(http.StatusOK, response) +} + // TransactionParams returns the suggested parameters for constructing a new transaction. // (GET /v2/transactions/params) func (v2 *Handlers) TransactionParams(ctx echo.Context) error { @@ -569,6 +590,63 @@ func (v2 *Handlers) GetPendingTransactions(ctx echo.Context, params generated.Ge return v2.getPendingTransactions(ctx, params.Max, params.Format, nil) } +// GetApplicationByID returns application information by app idx. +// (GET /v2/applications/{application-id}) +func (v2 *Handlers) GetApplicationByID(ctx echo.Context, applicationID uint64) error { + appIdx := basics.AppIndex(applicationID) + ledger := v2.Node.Ledger() + creator, ok, err := ledger.GetCreator(basics.CreatableIndex(appIdx), basics.AppCreatable) + if err != nil { + return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) + } + if !ok { + return notFound(ctx, errors.New(errAppDoesNotExist), errAppDoesNotExist, v2.Log) + } + + lastRound := ledger.Latest() + record, err := ledger.Lookup(lastRound, creator) + if err != nil { + return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) + } + + appParams, ok := record.AppParams[appIdx] + if !ok { + return notFound(ctx, errors.New(errAppDoesNotExist), errAppDoesNotExist, v2.Log) + } + app := AppParamsToApplication(creator.String(), appIdx, &appParams) + response := generated.ApplicationResponse(app) + return ctx.JSON(http.StatusOK, response) +} + +// GetAssetByID returns application information by app idx. +// (GET /v2/asset/{asset-id}) +func (v2 *Handlers) GetAssetByID(ctx echo.Context, assetID uint64) error { + assetIdx := basics.AssetIndex(assetID) + ledger := v2.Node.Ledger() + creator, ok, err := ledger.GetCreator(basics.CreatableIndex(assetIdx), basics.AssetCreatable) + if err != nil { + return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) + } + if !ok { + return notFound(ctx, errors.New(errAssetDoesNotExist), errAssetDoesNotExist, v2.Log) + } + + lastRound := ledger.Latest() + record, err := ledger.Lookup(lastRound, creator) + if err != nil { + return internalError(ctx, err, errFailedLookingUpLedger, v2.Log) + } + + assetParams, ok := record.AssetParams[assetIdx] + if !ok { + return notFound(ctx, errors.New(errAssetDoesNotExist), errAssetDoesNotExist, v2.Log) + } + + asset := AssetParamsToAsset(creator.String(), assetIdx, &assetParams) + response := generated.AssetResponse(asset) + return ctx.JSON(http.StatusOK, response) +} + // GetPendingTransactionsByAddress takes an Algorand address and returns its associated list of unconfirmed transactions currently in the transaction pool. // (GET /v2/accounts/{address}/transactions/pending) func (v2 *Handlers) GetPendingTransactionsByAddress(ctx echo.Context, addr string, params generated.GetPendingTransactionsByAddressParams) error { @@ -605,7 +683,7 @@ func (v2 *Handlers) TealCompile(ctx echo.Context) error { pd := logic.HashProgram(program) addr := basics.Address(pd) - response := generated.PostCompileResponse{ + response := generated.CompileResponse{ Hash: addr.String(), Result: base64.StdEncoding.EncodeToString(program), } diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 6f023fdf0f..17adf2a182 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -27,9 +27,12 @@ import ( "github.com/stretchr/testify/require" v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" + "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" "github.com/algorand/go-algorand/data/account" + "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/logging" "github.com/algorand/go-algorand/protocol" ) @@ -54,6 +57,8 @@ func setupTestForMethodGet(t *testing.T) (v2.Handlers, echo.Context, *httptest.R } func TestSimpleMockBuilding(t *testing.T) { + t.Parallel() + handler, _, _, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() require.Equal(t, t.Name(), handler.Node.GenesisID()) @@ -62,7 +67,7 @@ func TestSimpleMockBuilding(t *testing.T) { func accountInformationTest(t *testing.T, address string, expectedCode int) { handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() - err := handler.AccountInformation(c, address) + err := handler.AccountInformation(c, address, generatedV2.AccountInformationParams{}) require.NoError(t, err) require.Equal(t, expectedCode, rec.Code) if address == poolAddr.String() { @@ -75,6 +80,8 @@ func accountInformationTest(t *testing.T, address string, expectedCode int) { } func TestAccountInformation(t *testing.T) { + t.Parallel() + accountInformationTest(t, poolAddr.String(), 200) accountInformationTest(t, "bad account", 400) } @@ -88,6 +95,8 @@ func getBlockTest(t *testing.T, blockNum uint64, format string, expectedCode int } func TestGetBlock(t *testing.T) { + t.Parallel() + getBlockTest(t, 0, "json", 200) getBlockTest(t, 0, "msgpack", 200) getBlockTest(t, 1, "json", 500) @@ -95,6 +104,8 @@ func TestGetBlock(t *testing.T) { } func TestGetSupply(t *testing.T) { + t.Parallel() + handler, c, _, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.GetSupply(c) @@ -102,6 +113,8 @@ func TestGetSupply(t *testing.T) { } func TestGetStatus(t *testing.T) { + t.Parallel() + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.GetStatus(c) @@ -130,6 +143,8 @@ func TestGetStatus(t *testing.T) { } func TestGetStatusAfterBlock(t *testing.T) { + t.Parallel() + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.WaitForBlock(c, 0) @@ -140,6 +155,8 @@ func TestGetStatusAfterBlock(t *testing.T) { } func TestGetTransactionParams(t *testing.T) { + t.Parallel() + handler, c, rec, _, _, releasefunc := setupTestForMethodGet(t) defer releasefunc() err := handler.TransactionParams(c) @@ -161,6 +178,8 @@ func pendingTransactionInformationTest(t *testing.T, txidToUse int, format strin } func TestPendingTransactionInformation(t *testing.T) { + t.Parallel() + pendingTransactionInformationTest(t, 0, "json", 200) pendingTransactionInformationTest(t, 0, "msgpack", 200) pendingTransactionInformationTest(t, -1, "json", 400) @@ -177,6 +196,8 @@ func getPendingTransactionsTest(t *testing.T, format string, expectedCode int) { } func TestPendingTransactions(t *testing.T) { + t.Parallel() + getPendingTransactionsTest(t, "json", 200) getPendingTransactionsTest(t, "msgpack", 200) getPendingTransactionsTest(t, "bad format", 400) @@ -196,6 +217,8 @@ func pendingTransactionsByAddressTest(t *testing.T, rootkeyToUse int, format str } func TestPendingTransactionsByAddress(t *testing.T) { + t.Parallel() + pendingTransactionsByAddressTest(t, 0, "json", 200) pendingTransactionsByAddressTest(t, 0, "msgpack", 200) pendingTransactionsByAddressTest(t, 0, "bad format", 400) @@ -231,6 +254,8 @@ func postTransactionTest(t *testing.T, txnToUse, expectedCode int) { } func TestPostTransaction(t *testing.T) { + t.Parallel() + postTransactionTest(t, -1, 400) postTransactionTest(t, 0, 200) } @@ -258,6 +283,8 @@ func startCatchupTest(t *testing.T, catchpoint string, expectedCode int) { } func TestStartCatchup(t *testing.T) { + t.Parallel() + goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" startCatchupTest(t, goodCatchPoint, 200) badCatchPoint := "bad catchpoint" @@ -287,6 +314,8 @@ func abortCatchupTest(t *testing.T, catchpoint string, expectedCode int) { } func TestAbortCatchup(t *testing.T) { + t.Parallel() + goodCatchPoint := "5894690#DVFRZUYHEFKRLK5N6DNJRR4IABEVN2D6H76F3ZSEPIE6MKXMQWQA" abortCatchupTest(t, goodCatchPoint, 200) badCatchPoint := "bad catchpoint" @@ -317,6 +346,8 @@ func tealCompileTest(t *testing.T, bytesToUse []byte, expectedCode int, enableDe } func TestTealCompile(t *testing.T) { + t.Parallel() + tealCompileTest(t, nil, 200, true) // nil program should work goodProgram := `int 1` goodProgramBytes := []byte(goodProgram) @@ -326,3 +357,128 @@ func TestTealCompile(t *testing.T) { badProgramBytes := []byte(badProgram) tealCompileTest(t, badProgramBytes, 400, true) } + +func tealDryrunTest( + t *testing.T, obj *generatedV2.DryrunRequest, format string, + expCode int, expResult string, enableDeveloperAPI bool, +) (response generatedV2.DryrunResponse) { + numAccounts := 1 + numTransactions := 1 + offlineAccounts := true + mockLedger, _, _, _, releasefunc := testingenv(t, numAccounts, numTransactions, offlineAccounts) + defer releasefunc() + dummyShutdownChan := make(chan struct{}) + mockNode := makeMockNode(mockLedger, t.Name()) + mockNode.config.EnableDeveloperAPI = enableDeveloperAPI + handler := v2.Handlers{ + Node: &mockNode, + Log: logging.Base(), + Shutdown: dummyShutdownChan, + } + + var data []byte + if format == "json" { + data = protocol.EncodeJSON(obj) + } else { + obj2, err := v2.DryrunRequestFromGenerated(obj) + require.NoError(t, err) + data = protocol.EncodeReflect(&obj2) + } + + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(data)) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + err := handler.TealDryrun(c) + require.NoError(t, err) + require.Equal(t, expCode, rec.Code) + if rec.Code == 200 { + data = rec.Body.Bytes() + err = protocol.DecodeJSON(data, &response) + require.NoError(t, err, string(data)) + + require.GreaterOrEqual(t, len(response.Txns), 1) + messages := *response.Txns[0].AppCallMessages + require.NotNil(t, response.Txns[0].AppCallMessages) + require.GreaterOrEqual(t, len(messages), 1) + require.Equal(t, expResult, messages[len(messages)-1]) + } + return +} + +func TestTealDryrun(t *testing.T) { + t.Parallel() + + var gdr generated.DryrunRequest + txns := []transactions.SignedTxn{ + { + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 1, + ApprovalProgram: []byte{1, 2, 3}, + ApplicationArgs: [][]byte{ + []byte("check"), + []byte("bar"), + }, + }, + }, + }, + } + for i := range txns { + enc := protocol.EncodeJSON(&txns[i]) + gdr.Txns = append(gdr.Txns, enc) + } + + sucProgram, err := logic.AssembleString("int 1") + require.NoError(t, err) + + failProgram, err := logic.AssembleString("int 0") + require.NoError(t, err) + + gdr.Apps = []generated.Application{ + { + Id: 1, + Params: generated.ApplicationParams{ + ApprovalProgram: sucProgram, + }, + }, + } + localv := make(generated.TealKeyValueStore, 1) + localv[0] = generated.TealKeyValue{ + Key: "foo", + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + } + + gdr.Accounts = []generated.Account{ + { + Address: basics.Address{}.String(), + AppsLocalState: &[]generated.ApplicationLocalStates{ + { + Id: 1, + State: generated.ApplicationLocalState{ + KeyValue: localv, + }, + }, + }, + }, + } + + tealDryrunTest(t, &gdr, "json", 200, "PASS", true) + tealDryrunTest(t, &gdr, "msgp", 200, "PASS", true) + tealDryrunTest(t, &gdr, "msgp", 404, "", false) + + gdr.ProtocolVersion = "unk" + tealDryrunTest(t, &gdr, "json", 400, "", true) + gdr.ProtocolVersion = "" + ddr := tealDryrunTest(t, &gdr, "json", 200, "PASS", true) + require.Equal(t, string(protocol.ConsensusCurrentVersion), ddr.ProtocolVersion) + gdr.ProtocolVersion = string(protocol.ConsensusFuture) + ddr = tealDryrunTest(t, &gdr, "json", 200, "PASS", true) + require.Equal(t, string(protocol.ConsensusFuture), ddr.ProtocolVersion) + + gdr.Apps[0].Params.ApprovalProgram = failProgram + tealDryrunTest(t, &gdr, "json", 200, "REJECT", true) + tealDryrunTest(t, &gdr, "msgp", 200, "REJECT", true) + tealDryrunTest(t, &gdr, "json", 404, "", false) +} diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 4617da6d8c..0435687158 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -58,6 +58,9 @@ var cannedStatusReportGolden = node.StatusReport{ var poolAddrRewardBaseGolden = uint64(0) var poolAddrAssetsGolden = make([]generatedV2.AssetHolding, 0) var poolAddrCreatedAssetsGolden = make([]generatedV2.Asset, 0) +var appLocalStates = make([]generatedV2.ApplicationLocalStates, 0) +var appsTotalSchema = generatedV2.ApplicationStateSchema{} +var appCreatedApps = make([]generatedV2.Application, 0) var poolAddrResponseGolden = generatedV2.AccountResponse{ Address: poolAddr.String(), Amount: 50000000000, @@ -66,6 +69,9 @@ var poolAddrResponseGolden = generatedV2.AccountResponse{ CreatedAssets: &poolAddrCreatedAssetsGolden, RewardBase: &poolAddrRewardBaseGolden, Status: "Not Participating", + AppsLocalState: &appLocalStates, + AppsTotalSchema: &appsTotalSchema, + CreatedApps: &appCreatedApps, } // ordinarily mockNode would live in `components/mocks` diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index 841f10b597..399efbd44e 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -168,3 +168,13 @@ func encode(handle codec.Handle, obj interface{}) ([]byte, error) { } return output, nil } + +func decode(handle codec.Handle, data []byte, v interface{}) error { + enc := codec.NewDecoderBytes(data, handle) + + err := enc.Decode(v) + if err != nil { + return fmt.Errorf("failed to decode object: %v", err) + } + return nil +} diff --git a/daemon/algod/api/spec/v1/model.go b/daemon/algod/api/spec/v1/model.go index e8f611e99a..7f70e5f82a 100644 --- a/daemon/algod/api/spec/v1/model.go +++ b/daemon/algod/api/spec/v1/model.go @@ -105,6 +105,116 @@ type Participation struct { // Round and Address fields are redundant if Partici VoteKeyDilution uint64 `json:"votekd"` } +// TealValue represents a value stored in a TEAL key/value store. It includes +// type information to disambiguate empty values from each other. +// +// swagger: model TealValue +type TealValue struct { + // Type is the type of the value, either "b" for a TEAL byte slice or + // "u" for a TEAL uint + // + // required: true + Type string `json:"t"` + + // Bytes is the value of a TEAL byte slice + // + // required: true + Bytes string `json:"b,omitempty"` + + // Uint is the value of a TEAL uint + // + // required: true + Uint uint64 `json:"u,omitempty"` +} + +// AppParams stores the global information associated with the application, +// including its current logic, state schemas, and global state. +// +// swagger: model AppParams +type AppParams struct { + // Creator is the creator of the application, whose account stores the + // AppParams + // + // required: true + Creator string `json:"creator,omitempty"` + + // ApprovalProgram is the logic that executes for each ApplicationCall + // transaction besides those where OnCompletion == ClearStateOC. It can + // read and write global state for the application, as well as + // account-specific local state. + // + // required: true + ApprovalProgram string `json:"approvprog"` + + // ClearStateProgram is the logic that executes for each ApplicationCall + // transaction where OnCompletion == ClearStateOC. It can read and write + // global state for the application, as well as account-specific local + // state. However, it cannot reject the transaction. + // + // required: true + ClearStateProgram string `json:"clearprog"` + + // LocalStateSchema sets limits on the number of strings and integers + // that may be stored in an account's LocalState. for this application. + // The larger these limits are, the larger minimum balance must be + // maintained inside the account of any users who opt into this + // application. The LocalStateSchema is immutable. + // + // require: true + LocalStateSchema *StateSchema `json:"localschema"` + + // GlobalStateSchema sets limits on the number of strings and integers + // that may be stored in the GlobalState. The larger these limits are, + // the larger minimum balance must be maintained inside the creator's + // account (in order to 'pay' for the state that can be used). The + // GlobalStateSchema is immutable. + // + // require: true + GlobalStateSchema *StateSchema `json:"globalschema"` + + // GlobalState stores global keys and values associated with this + // application. It must respect the limits set by GlobalStateSchema. + // + // require: true + GlobalState map[string]TealValue `json:"globalstate"` +} + +// StateSchema represents a LocalStateSchema or GlobalStateSchema. These +// schemas determine how much storage may be used in a LocalState or +// GlobalState for an application. The more space used, the larger minimum +// balance must be maintained in the account holding the data. +// +// swagger: model StateSchema +type StateSchema struct { + // NumUint is the maximum number of TEAL uints that may be stored in + // the key/value store + // + // required: true + NumUint uint64 `json:"uints"` + + // NumByteSlice is the maximum number of TEAL byte slices that may be + // stored in the key/value store + // + // required: true + NumByteSlice uint64 `json:"byteslices"` +} + +// Application specifies both the unique identifier and the parameters for an +// application +// +// swagger:model Application +type Application struct { + // AppIndex is the unique application identifier + // + // required: true + AppIndex uint64 `json:"appidx"` + + // AppParams specifies the parameters of application referred to by AppIndex + // + // required: true + AppParams AppParams `json:"appparams"` +} + // Account Description // swagger:model Account type Account struct { @@ -165,9 +275,38 @@ type Account struct { // // required: false Assets map[uint64]AssetHolding `json:"assets,omitempty"` + + // AppLocalStates is a map of local states for applications this + // account has opted in to, as well as a copy of each application's + // LocalStateSchema + // + // required: false + AppLocalStates map[uint64]AppLocalState `json:"applocalstates,omitempty"` + + // AppParams is a map of application parameters for applications that + // were created by this account. These parameters include the + // application's global state map + // + // required: false + AppParams map[uint64]AppParams `json:"appparams,omitempty"` +} + +// AppLocalState holds the local key/value store of an application for an +// account that has opted in, as well as a copy of that application's +// LocalStateSchema +// +// swagger:model AppLocalState +type AppLocalState struct { + // Schema is a copy of the application's LocalStateSchema + Schema *StateSchema `json:"localschema"` + + // KeyValue is the key/value store representing the application's + // local state in this account + KeyValue map[string]TealValue `json:"localstate"` } // Asset specifies both the unique identifier and the parameters for an asset +// // swagger:model Asset type Asset struct { // AssetIndex is the unique asset identifier @@ -380,6 +519,11 @@ type Transaction struct { // required: false AssetFreeze *AssetFreezeTransactionType `json:"curfrz,omitempty"` + // ApplicationCall + // + // required: true + ApplicationCall *ApplicationCallTransactionType `json:"app,omitempty"` + // FromRewards is the amount of pending rewards applied to the From // account as part of this transaction. // @@ -478,6 +622,11 @@ type TransactionResults struct { // // required: false CreatedAssetIndex uint64 `json:"createdasset,omitempty"` + + // CreatedAppIndex indicates the app index of an app created by this txn + // + // required: false + CreatedAppIndex uint64 `json:"createdapp,omitempty"` } // AssetConfigTransactionType contains the additional fields for an asset config transaction @@ -542,6 +691,74 @@ type AssetFreezeTransactionType struct { NewFreezeStatus bool `json:"freeze"` } +// ApplicationCallTransactionType contains the additional fields for an ApplicationCall transaction +// swagger:model ApplicationCallTransactionType +type ApplicationCallTransactionType struct { + // ApplicationID is the application being interacted with, or 0 if + // creating a new application. + // + // required: true + ApplicationID uint64 `json:"id"` + + // Accounts lists the accounts (in addition to the sender) that may be + // accessed from the application's ApprovalProgram and ClearStateProgram. + // + // required: true + Accounts []string `json:"accounts"` + + // ForeignApps lists the applications (in addition to txn.ApplicationID) + // whose global states may be accessed by this application's + // ApprovalProgram and ClearStateProgram. The access is read-only. + // + // required: true + ForeignApps []uint64 `json:"foreignapps"` + + // ApplicationArgs lists some transaction-specific arguments accessible + // from application logic + // + // required: true + ApplicationArgs []string `json:"appargs"` + + // ApprovalProgram determines whether or not this ApplicationCall + // transaction will be approved or not. It does not execute when + // OnCompletion == ClearStateOC, because clearing local state is always + // allowed. + // + // required: true + ApprovalProgram string `json:"approvprog,omitempty"` + + // ClearStateProgram executes when an ApplicationCall transaction + // executes with OnCompletion == ClearStateOC. However, this program + // may not reject the transaction (only update state). If this program + // + // required: true + ClearStateProgram string `json:"clearprog,omitempty"` + + // GlobalStateSchema sets limits on the number of strings and integers + // that may be stored in the GlobalState. The larger these limits are, + // the larger minimum balance must be maintained inside the creator's + // account (in order to 'pay' for the state that can be used). The + // GlobalStateSchema is immutable. + // + // require: true + GlobalStateSchema *StateSchema `json:"globalschema,omitempty"` + + // LocalStateSchema sets limits on the number of strings and integers + // that may be stored in an account's LocalState. for this application. + // The larger these limits are, the larger minimum balance must be + // maintained inside the account of any users who opt into this + // application. The LocalStateSchema is immutable. + // + // require: true + LocalStateSchema *StateSchema `json:"localschema,omitempty"` + + // OnCompletion specifies what side effects this transaction will have + // if it successfully makes it into a block. + // + // require: true + OnCompletion string `json:"oncompletion"` +} + // TransactionList contains a list of transactions // swagger:model TransactionList type TransactionList struct { @@ -554,7 +771,7 @@ type TransactionList struct { // AssetList contains a list of assets // swagger:model AssetList type AssetList struct { - // AssetList is a list of assets + // Assets is a list of assets // // required: true Assets []Asset `json:"assets,omitempty"` diff --git a/data/basics/msgp_gen.go b/data/basics/msgp_gen.go index fce4eb73d3..3d8c7c2508 100644 --- a/data/basics/msgp_gen.go +++ b/data/basics/msgp_gen.go @@ -147,6 +147,14 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // +// StateSchemas +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // Status // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -1746,41 +1754,41 @@ func (z *AppParams) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values zb0003Len := uint32(5) - var zb0003Mask uint8 /* 6 bits */ + var zb0003Mask uint8 /* 7 bits */ if len((*z).ApprovalProgram) == 0 { - zb0003Len-- - zb0003Mask |= 0x2 - } - if len((*z).ClearStateProgram) == 0 { zb0003Len-- zb0003Mask |= 0x4 } - if len((*z).GlobalState) == 0 { + if len((*z).ClearStateProgram) == 0 { zb0003Len-- zb0003Mask |= 0x8 } - if ((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0) { + if len((*z).GlobalState) == 0 { zb0003Len-- zb0003Mask |= 0x10 } - if ((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0) { + if ((*z).StateSchemas.GlobalStateSchema.NumUint == 0) && ((*z).StateSchemas.GlobalStateSchema.NumByteSlice == 0) { zb0003Len-- zb0003Mask |= 0x20 } + if ((*z).StateSchemas.LocalStateSchema.NumUint == 0) && ((*z).StateSchemas.LocalStateSchema.NumByteSlice == 0) { + zb0003Len-- + zb0003Mask |= 0x40 + } // variable map header, size zb0003Len o = append(o, 0x80|uint8(zb0003Len)) if zb0003Len != 0 { - if (zb0003Mask & 0x2) == 0 { // if not empty + if (zb0003Mask & 0x4) == 0 { // if not empty // string "approv" o = append(o, 0xa6, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76) o = msgp.AppendBytes(o, (*z).ApprovalProgram) } - if (zb0003Mask & 0x4) == 0 { // if not empty + if (zb0003Mask & 0x8) == 0 { // if not empty // string "clearp" o = append(o, 0xa6, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x70) o = msgp.AppendBytes(o, (*z).ClearStateProgram) } - if (zb0003Mask & 0x8) == 0 { // if not empty + if (zb0003Mask & 0x10) == 0 { // if not empty // string "gs" o = append(o, 0xa2, 0x67, 0x73) if (*z).GlobalState == nil { @@ -1804,17 +1812,17 @@ func (z *AppParams) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0003Mask & 0x10) == 0 { // if not empty + if (zb0003Mask & 0x20) == 0 { // if not empty // string "gsch" o = append(o, 0xa4, 0x67, 0x73, 0x63, 0x68) // omitempty: check for empty values zb0004Len := uint32(2) var zb0004Mask uint8 /* 3 bits */ - if (*z).GlobalStateSchema.NumByteSlice == 0 { + if (*z).StateSchemas.GlobalStateSchema.NumByteSlice == 0 { zb0004Len-- zb0004Mask |= 0x2 } - if (*z).GlobalStateSchema.NumUint == 0 { + if (*z).StateSchemas.GlobalStateSchema.NumUint == 0 { zb0004Len-- zb0004Mask |= 0x4 } @@ -1823,25 +1831,25 @@ func (z *AppParams) MarshalMsg(b []byte) (o []byte, err error) { if (zb0004Mask & 0x2) == 0 { // if not empty // string "nbs" o = append(o, 0xa3, 0x6e, 0x62, 0x73) - o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumByteSlice) + o = msgp.AppendUint64(o, (*z).StateSchemas.GlobalStateSchema.NumByteSlice) } if (zb0004Mask & 0x4) == 0 { // if not empty // string "nui" o = append(o, 0xa3, 0x6e, 0x75, 0x69) - o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumUint) + o = msgp.AppendUint64(o, (*z).StateSchemas.GlobalStateSchema.NumUint) } } - if (zb0003Mask & 0x20) == 0 { // if not empty + if (zb0003Mask & 0x40) == 0 { // if not empty // string "lsch" o = append(o, 0xa4, 0x6c, 0x73, 0x63, 0x68) // omitempty: check for empty values zb0005Len := uint32(2) var zb0005Mask uint8 /* 3 bits */ - if (*z).LocalStateSchema.NumByteSlice == 0 { + if (*z).StateSchemas.LocalStateSchema.NumByteSlice == 0 { zb0005Len-- zb0005Mask |= 0x2 } - if (*z).LocalStateSchema.NumUint == 0 { + if (*z).StateSchemas.LocalStateSchema.NumUint == 0 { zb0005Len-- zb0005Mask |= 0x4 } @@ -1850,12 +1858,12 @@ func (z *AppParams) MarshalMsg(b []byte) (o []byte, err error) { if (zb0005Mask & 0x2) == 0 { // if not empty // string "nbs" o = append(o, 0xa3, 0x6e, 0x62, 0x73) - o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumByteSlice) + o = msgp.AppendUint64(o, (*z).StateSchemas.LocalStateSchema.NumByteSlice) } if (zb0005Mask & 0x4) == 0 { // if not empty // string "nui" o = append(o, 0xa3, 0x6e, 0x75, 0x69) - o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumUint) + o = msgp.AppendUint64(o, (*z).StateSchemas.LocalStateSchema.NumUint) } } } @@ -1921,30 +1929,66 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { var zb0007 int var zb0008 bool zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState") + return + } + if zb0007 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0007), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "struct-from-array", "GlobalState") + return + } + if zb0008 { + (*z).GlobalState = nil + } else if (*z).GlobalState == nil { + (*z).GlobalState = make(TealKeyValue, zb0007) + } + for zb0007 > 0 { + var zb0001 string + var zb0002 TealValue + zb0007-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalState", zb0001) + return + } + (*z).GlobalState[zb0001] = zb0002 + } + } + if zb0003 > 0 { + zb0003-- + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } - if zb0007 > 0 { - zb0007-- - (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if zb0009 > 0 { + zb0009-- + (*z).StateSchemas.LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumUint") return } } - if zb0007 > 0 { - zb0007-- - (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if zb0009 > 0 { + zb0009-- + (*z).StateSchemas.LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0007 > 0 { - err = msgp.ErrTooManyArrayFields(zb0007) + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array") return @@ -1955,11 +1999,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } - if zb0008 { - (*z).LocalStateSchema = StateSchema{} + if zb0010 { + (*z).StateSchemas.LocalStateSchema = StateSchema{} } - for zb0007 > 0 { - zb0007-- + for zb0009 > 0 { + zb0009-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") @@ -1967,13 +2011,13 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "nui": - (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumUint") return } case "nbs": - (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumByteSlice") return @@ -1990,33 +2034,33 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } if zb0003 > 0 { zb0003-- - var zb0009 int - var zb0010 bool - zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0011 int + var zb0012 bool + zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } - if zb0009 > 0 { - zb0009-- - (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if zb0011 > 0 { + zb0011-- + (*z).StateSchemas.GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumUint") return } } - if zb0009 > 0 { - zb0009-- - (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if zb0011 > 0 { + zb0011-- + (*z).StateSchemas.GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0009 > 0 { - err = msgp.ErrTooManyArrayFields(zb0009) + if zb0011 > 0 { + err = msgp.ErrTooManyArrayFields(zb0011) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array") return @@ -2027,11 +2071,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } - if zb0010 { - (*z).GlobalStateSchema = StateSchema{} + if zb0012 { + (*z).StateSchemas.GlobalStateSchema = StateSchema{} } - for zb0009 > 0 { - zb0009-- + for zb0011 > 0 { + zb0011-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") @@ -2039,13 +2083,13 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "nui": - (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumUint") return } case "nbs": - (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumByteSlice") return @@ -2060,42 +2104,6 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - if zb0003 > 0 { - zb0003-- - var zb0011 int - var zb0012 bool - zb0011, zb0012, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalState") - return - } - if zb0011 > encodedMaxKeyValueEntries { - err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxKeyValueEntries)) - err = msgp.WrapError(err, "struct-from-array", "GlobalState") - return - } - if zb0012 { - (*z).GlobalState = nil - } else if (*z).GlobalState == nil { - (*z).GlobalState = make(TealKeyValue, zb0011) - } - for zb0011 > 0 { - var zb0001 string - var zb0002 TealValue - zb0011-- - zb0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalState") - return - } - bts, err = zb0002.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "GlobalState", zb0001) - return - } - (*z).GlobalState[zb0001] = zb0002 - } - } if zb0003 > 0 { err = msgp.ErrTooManyArrayFields(zb0003) if err != nil { @@ -2151,34 +2159,68 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "ClearStateProgram") return } - case "lsch": + case "gs": var zb0015 int var zb0016 bool zb0015, zb0016, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState") + return + } + if zb0015 > encodedMaxKeyValueEntries { + err = msgp.ErrOverflow(uint64(zb0015), uint64(encodedMaxKeyValueEntries)) + err = msgp.WrapError(err, "GlobalState") + return + } + if zb0016 { + (*z).GlobalState = nil + } else if (*z).GlobalState == nil { + (*z).GlobalState = make(TealKeyValue, zb0015) + } + for zb0015 > 0 { + var zb0001 string + var zb0002 TealValue + zb0015-- + zb0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState") + return + } + bts, err = zb0002.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalState", zb0001) + return + } + (*z).GlobalState[zb0001] = zb0002 + } + case "lsch": + var zb0017 int + var zb0018 bool + zb0017, zb0018, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0015, zb0016, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema") return } - if zb0015 > 0 { - zb0015-- - (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if zb0017 > 0 { + zb0017-- + (*z).StateSchemas.LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumUint") return } } - if zb0015 > 0 { - zb0015-- - (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if zb0017 > 0 { + zb0017-- + (*z).StateSchemas.LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0015 > 0 { - err = msgp.ErrTooManyArrayFields(zb0015) + if zb0017 > 0 { + err = msgp.ErrTooManyArrayFields(zb0017) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array") return @@ -2189,11 +2231,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "LocalStateSchema") return } - if zb0016 { - (*z).LocalStateSchema = StateSchema{} + if zb0018 { + (*z).StateSchemas.LocalStateSchema = StateSchema{} } - for zb0015 > 0 { - zb0015-- + for zb0017 > 0 { + zb0017-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema") @@ -2201,13 +2243,13 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "nui": - (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "NumUint") return } case "nbs": - (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "LocalStateSchema", "NumByteSlice") return @@ -2222,33 +2264,33 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "gsch": - var zb0017 int - var zb0018 bool - zb0017, zb0018, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0017, zb0018, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema") return } - if zb0017 > 0 { - zb0017-- - (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if zb0019 > 0 { + zb0019-- + (*z).StateSchemas.GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumUint") return } } - if zb0017 > 0 { - zb0017-- - (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if zb0019 > 0 { + zb0019-- + (*z).StateSchemas.GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumByteSlice") return } } - if zb0017 > 0 { - err = msgp.ErrTooManyArrayFields(zb0017) + if zb0019 > 0 { + err = msgp.ErrTooManyArrayFields(zb0019) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array") return @@ -2259,11 +2301,11 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err, "GlobalStateSchema") return } - if zb0018 { - (*z).GlobalStateSchema = StateSchema{} + if zb0020 { + (*z).StateSchemas.GlobalStateSchema = StateSchema{} } - for zb0017 > 0 { - zb0017-- + for zb0019 > 0 { + zb0019-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema") @@ -2271,13 +2313,13 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } switch string(field) { case "nui": - (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "NumUint") return } case "nbs": - (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + (*z).StateSchemas.GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "GlobalStateSchema", "NumByteSlice") return @@ -2291,40 +2333,6 @@ func (z *AppParams) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - case "gs": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadMapHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalState") - return - } - if zb0019 > encodedMaxKeyValueEntries { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxKeyValueEntries)) - err = msgp.WrapError(err, "GlobalState") - return - } - if zb0020 { - (*z).GlobalState = nil - } else if (*z).GlobalState == nil { - (*z).GlobalState = make(TealKeyValue, zb0019) - } - for zb0019 > 0 { - var zb0001 string - var zb0002 TealValue - zb0019-- - zb0001, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalState") - return - } - bts, err = zb0002.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "GlobalState", zb0001) - return - } - (*z).GlobalState[zb0001] = zb0002 - } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -2345,7 +2353,7 @@ func (_ *AppParams) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *AppParams) Msgsize() (s int) { - s = 1 + 7 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 7 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 3 + msgp.MapHeaderSize + s = 1 + 7 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 7 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) + 3 + msgp.MapHeaderSize if (*z).GlobalState != nil { for zb0001, zb0002 := range (*z).GlobalState { _ = zb0001 @@ -2353,12 +2361,13 @@ func (z *AppParams) Msgsize() (s int) { s += 0 + msgp.StringPrefixSize + len(zb0001) + zb0002.Msgsize() } } + s += 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value func (z *AppParams) MsgIsZero() bool { - return (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) && (((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0)) && (((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0)) && (len((*z).GlobalState) == 0) + return (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) && (len((*z).GlobalState) == 0) && (((*z).StateSchemas.LocalStateSchema.NumUint == 0) && ((*z).StateSchemas.LocalStateSchema.NumByteSlice == 0)) && (((*z).StateSchemas.GlobalStateSchema.NumUint == 0) && ((*z).StateSchemas.GlobalStateSchema.NumByteSlice == 0)) } // MarshalMsg implements msgp.Marshaler @@ -4860,6 +4869,435 @@ func (z *StateSchema) MsgIsZero() bool { return ((*z).NumUint == 0) && ((*z).NumByteSlice == 0) } +// MarshalMsg implements msgp.Marshaler +func (z *StateSchemas) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0001Len := uint32(2) + var zb0001Mask uint8 /* 3 bits */ + if ((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0) { + zb0001Len-- + zb0001Mask |= 0x2 + } + if ((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0) { + zb0001Len-- + zb0001Mask |= 0x4 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "gsch" + o = append(o, 0xa4, 0x67, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0002Len := uint32(2) + var zb0002Mask uint8 /* 3 bits */ + if (*z).GlobalStateSchema.NumByteSlice == 0 { + zb0002Len-- + zb0002Mask |= 0x2 + } + if (*z).GlobalStateSchema.NumUint == 0 { + zb0002Len-- + zb0002Mask |= 0x4 + } + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumByteSlice) + } + if (zb0002Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).GlobalStateSchema.NumUint) + } + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "lsch" + o = append(o, 0xa4, 0x6c, 0x73, 0x63, 0x68) + // omitempty: check for empty values + zb0003Len := uint32(2) + var zb0003Mask uint8 /* 3 bits */ + if (*z).LocalStateSchema.NumByteSlice == 0 { + zb0003Len-- + zb0003Mask |= 0x2 + } + if (*z).LocalStateSchema.NumUint == 0 { + zb0003Len-- + zb0003Mask |= 0x4 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "nbs" + o = append(o, 0xa3, 0x6e, 0x62, 0x73) + o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumByteSlice) + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "nui" + o = append(o, 0xa3, 0x6e, 0x75, 0x69) + o = msgp.AppendUint64(o, (*z).LocalStateSchema.NumUint) + } + } + } + return +} + +func (_ *StateSchemas) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*StateSchemas) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *StateSchemas) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0001 > 0 { + zb0001-- + var zb0003 int + var zb0004 bool + zb0003, zb0004, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0003, zb0004, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + if zb0003 > 0 { + zb0003-- + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0003 > 0 { + zb0003-- + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0003 > 0 { + err = msgp.ErrTooManyArrayFields(zb0003) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + if zb0004 { + (*z).LocalStateSchema = StateSchema{} + } + for zb0003 > 0 { + zb0003-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumUint") + return + } + case "nbs": + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") + return + } + } + } + } + } + if zb0001 > 0 { + zb0001-- + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + if zb0005 > 0 { + zb0005-- + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0005 > 0 { + zb0005-- + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0005 > 0 { + err = msgp.ErrTooManyArrayFields(zb0005) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + if zb0006 { + (*z).GlobalStateSchema = StateSchema{} + } + for zb0005 > 0 { + zb0005-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumUint") + return + } + case "nbs": + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") + return + } + } + } + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0002 { + (*z) = StateSchemas{} + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "lsch": + var zb0007 int + var zb0008 bool + zb0007, zb0008, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + if zb0007 > 0 { + zb0007-- + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0007 > 0 { + zb0007-- + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0007 > 0 { + err = msgp.ErrTooManyArrayFields(zb0007) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + if zb0008 { + (*z).LocalStateSchema = StateSchema{} + } + for zb0007 > 0 { + zb0007-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).LocalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "NumUint") + return + } + case "nbs": + (*z).LocalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "LocalStateSchema") + return + } + } + } + } + case "gsch": + var zb0009 int + var zb0010 bool + zb0009, zb0010, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + if zb0009 > 0 { + zb0009-- + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumUint") + return + } + } + if zb0009 > 0 { + zb0009-- + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array", "NumByteSlice") + return + } + } + if zb0009 > 0 { + err = msgp.ErrTooManyArrayFields(zb0009) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + if zb0010 { + (*z).GlobalStateSchema = StateSchema{} + } + for zb0009 > 0 { + zb0009-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + switch string(field) { + case "nui": + (*z).GlobalStateSchema.NumUint, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "NumUint") + return + } + case "nbs": + (*z).GlobalStateSchema.NumByteSlice, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema", "NumByteSlice") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "GlobalStateSchema") + return + } + } + } + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (_ *StateSchemas) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*StateSchemas) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *StateSchemas) Msgsize() (s int) { + s = 1 + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + 5 + 1 + 4 + msgp.Uint64Size + 4 + msgp.Uint64Size + return +} + +// MsgIsZero returns whether this is a zero value +func (z *StateSchemas) MsgIsZero() bool { + return (((*z).LocalStateSchema.NumUint == 0) && ((*z).LocalStateSchema.NumByteSlice == 0)) && (((*z).GlobalStateSchema.NumUint == 0) && ((*z).GlobalStateSchema.NumByteSlice == 0)) +} + // MarshalMsg implements msgp.Marshaler func (z Status) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) diff --git a/data/basics/msgp_gen_test.go b/data/basics/msgp_gen_test.go index 64a4e7ce1d..a80da6ab45 100644 --- a/data/basics/msgp_gen_test.go +++ b/data/basics/msgp_gen_test.go @@ -569,6 +569,68 @@ func BenchmarkUnmarshalStateSchema(b *testing.B) { } } +func TestMarshalUnmarshalStateSchemas(t *testing.T) { + v := StateSchemas{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingStateSchemas(t *testing.T) { + protocol.RunEncodingTest(t, &StateSchemas{}) +} + +func BenchmarkMarshalMsgStateSchemas(b *testing.B) { + v := StateSchemas{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgStateSchemas(b *testing.B) { + v := StateSchemas{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalStateSchemas(b *testing.B) { + v := StateSchemas{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + func TestMarshalUnmarshalTealKeyValue(t *testing.T) { v := TealKeyValue{} bts, err := v.MarshalMsg(nil) diff --git a/data/basics/teal.go b/data/basics/teal.go index ed774c9c8a..ca6c5d43b7 100644 --- a/data/basics/teal.go +++ b/data/basics/teal.go @@ -286,30 +286,39 @@ func (tk TealKeyValue) Clone() TealKeyValue { return res } -// SatisfiesSchema returns an error indicating whether or not a particular -// TealKeyValue store meets the requirements set by a StateSchema on how -// many values of each type are allowed -func (tk TealKeyValue) SatisfiesSchema(schema StateSchema) error { - // Count all of the types in the key/value store - var uintCount, bytesCount uint64 +// ToStateSchema calculates the number of each value type in a TealKeyValue and +// reprsents the result as a StateSchema +func (tk TealKeyValue) ToStateSchema() (schema StateSchema, err error) { for _, value := range tk { switch value.Type { case TealBytesType: - bytesCount++ + schema.NumByteSlice++ case TealUintType: - uintCount++ + schema.NumUint++ default: - // Shouldn't happen - return fmt.Errorf("unknown type %v", value.Type) + err = fmt.Errorf("unknown type %v", value.Type) + return StateSchema{}, err } } + return schema, nil +} + +// SatisfiesSchema returns an error indicating whether or not a particular +// TealKeyValue store meets the requirements set by a StateSchema on how +// many values of each type are allowed +func (tk TealKeyValue) SatisfiesSchema(schema StateSchema) error { + calc, err := tk.ToStateSchema() + if err != nil { + return err + } // Check against the schema - if uintCount > schema.NumUint { - return fmt.Errorf("store integer count %d exceeds schema integer count %d", uintCount, schema.NumUint) + if calc.NumUint > schema.NumUint { + return fmt.Errorf("store integer count %d exceeds schema integer count %d", calc.NumUint, schema.NumUint) } - if bytesCount > schema.NumByteSlice { - return fmt.Errorf("store bytes count %d exceeds schema bytes count %d", bytesCount, schema.NumByteSlice) + if calc.NumByteSlice > schema.NumByteSlice { + return fmt.Errorf("store bytes count %d exceeds schema bytes count %d", calc.NumByteSlice, schema.NumByteSlice) } + return nil } diff --git a/data/basics/userBalance.go b/data/basics/userBalance.go index b14fdcafb5..1f56369d20 100644 --- a/data/basics/userBalance.go +++ b/data/basics/userBalance.go @@ -17,6 +17,7 @@ package basics import ( + "fmt" "reflect" "github.com/algorand/go-algorand/config" @@ -38,7 +39,7 @@ const ( // MaxEncodedAccountDataSize is a rough estimate for the worst-case scenario we're going to have of the account data and address serialized. // this number is verified by the TestEncodedAccountDataSize function. - MaxEncodedAccountDataSize = 324205 + MaxEncodedAccountDataSize = 750000 // encodedMaxAssetsPerAccount is the decoder limit of number of assets stored per account. // it's being verified by the unit test TestEncodedAccountAllocationBounds to align @@ -76,6 +77,21 @@ func (s Status) String() string { return "" } +// UnmarshalStatus decodes string status value back to Status constant +func UnmarshalStatus(value string) (s Status, err error) { + switch value { + case "Offline": + s = Offline + case "Online": + s = Online + case "Not Participating": + s = NotParticipating + default: + err = fmt.Errorf("unknown account status: %v", value) + } + return +} + // AccountData contains the data associated with a given address. // // This includes the account balance, delegation keys, delegation status, and a custom note. @@ -200,12 +216,19 @@ type AppLocalState struct { type AppParams struct { _struct struct{} `codec:",omitempty,omitemptyarray"` - ApprovalProgram []byte `codec:"approv,allocbound=config.MaxAppProgramLen"` - ClearStateProgram []byte `codec:"clearp,allocbound=config.MaxAppProgramLen"` + ApprovalProgram []byte `codec:"approv,allocbound=config.MaxAppProgramLen"` + ClearStateProgram []byte `codec:"clearp,allocbound=config.MaxAppProgramLen"` + GlobalState TealKeyValue `codec:"gs"` + StateSchemas +} + +// StateSchemas is a thin wrapper around the LocalStateSchema and the +// GlobalStateSchema, since they are often needed together +type StateSchemas struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + LocalStateSchema StateSchema `codec:"lsch"` GlobalStateSchema StateSchema `codec:"gsch"` - - GlobalState TealKeyValue `codec:"gs"` } // Clone returns a copy of some AppParams that may be modified without diff --git a/data/basics/userBalance_test.go b/data/basics/userBalance_test.go index 9d0ffcf0bc..bc23ac8890 100644 --- a/data/basics/userBalance_test.go +++ b/data/basics/userBalance_test.go @@ -101,6 +101,10 @@ func makeString(len int) string { func TestEncodedAccountDataSize(t *testing.T) { oneTimeSecrets := crypto.GenerateOneTimeSignatureSecrets(0, 1) vrfSecrets := crypto.GenerateVRFSecrets() + maxStateSchema := StateSchema{ + NumUint: 0x1234123412341234, + NumByteSlice: 0x1234123412341234, + } ad := AccountData{ Status: NotParticipating, MicroAlgos: MicroAlgos{}, @@ -113,9 +117,14 @@ func TestEncodedAccountDataSize(t *testing.T) { VoteKeyDilution: 0x1234123412341234, AssetParams: make(map[AssetIndex]AssetParams), Assets: make(map[AssetIndex]AssetHolding), + AppLocalStates: make(map[AppIndex]AppLocalState), + AppParams: make(map[AppIndex]AppParams), + TotalAppSchema: maxStateSchema, AuthAddr: Address(crypto.Hash([]byte{1, 2, 3, 4})), } - currentConsensusParams := config.Consensus[protocol.ConsensusCurrentVersion] + + // TODO after applications enabled: change back to protocol.ConsensusCurrentVersion + currentConsensusParams := config.Consensus[protocol.ConsensusFuture] for assetCreatorAssets := 0; assetCreatorAssets < currentConsensusParams.MaxAssetsPerAccount; assetCreatorAssets++ { ap := AssetParams{ @@ -142,9 +151,52 @@ func TestEncodedAccountDataSize(t *testing.T) { ad.Assets[AssetIndex(0x1234123412341234-assetHolderAssets)] = ah } + maxProg := []byte(makeString(currentConsensusParams.MaxAppProgramLen)) + maxGlobalState := make(TealKeyValue, currentConsensusParams.MaxGlobalSchemaEntries) + maxLocalState := make(TealKeyValue, currentConsensusParams.MaxLocalSchemaEntries) + maxValue := TealValue{ + Type: TealBytesType, + Bytes: makeString(currentConsensusParams.MaxAppBytesValueLen), + } + + for globalKey := uint64(0); globalKey < currentConsensusParams.MaxGlobalSchemaEntries; globalKey++ { + prefix := fmt.Sprintf("%d|", globalKey) + padding := makeString(currentConsensusParams.MaxAppKeyLen - len(prefix)) + maxKey := prefix + padding + maxGlobalState[maxKey] = maxValue + } + + for localKey := uint64(0); localKey < currentConsensusParams.MaxLocalSchemaEntries; localKey++ { + prefix := fmt.Sprintf("%d|", localKey) + padding := makeString(currentConsensusParams.MaxAppKeyLen - len(prefix)) + maxKey := prefix + padding + maxLocalState[maxKey] = maxValue + } + + for appCreatorApps := 0; appCreatorApps < currentConsensusParams.MaxAppsCreated; appCreatorApps++ { + ap := AppParams{ + ApprovalProgram: maxProg, + ClearStateProgram: maxProg, + GlobalState: maxGlobalState, + StateSchemas: StateSchemas{ + LocalStateSchema: maxStateSchema, + GlobalStateSchema: maxStateSchema, + }, + } + ad.AppParams[AppIndex(0x1234123412341234-appCreatorApps)] = ap + } + + for appHolderApps := 0; appHolderApps < currentConsensusParams.MaxAppsOptedIn; appHolderApps++ { + ls := AppLocalState{ + KeyValue: maxLocalState, + Schema: maxStateSchema, + } + ad.AppLocalStates[AppIndex(0x1234123412341234-appHolderApps)] = ls + } + encoded, err := ad.MarshalMsg(nil) require.NoError(t, err) - require.Equal(t, MaxEncodedAccountDataSize, len(encoded)) + require.GreaterOrEqual(t, MaxEncodedAccountDataSize, len(encoded)) } func TestEncodedAccountAllocationBounds(t *testing.T) { diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go index 0b64bd1ef1..8102d9010a 100644 --- a/data/pools/transactionPool.go +++ b/data/pools/transactionPool.go @@ -62,6 +62,11 @@ type TransactionPool struct { feeThresholdMultiplier uint64 statusCache *statusCache + assemblyMu deadlock.Mutex + assemblyCond sync.Cond + assemblyDeadline time.Time + assemblyResults poolAsmResults + // pendingMu protects pendingTxGroups and pendingTxids pendingMu deadlock.RWMutex pendingTxGroups [][]transactions.SignedTxn @@ -84,6 +89,9 @@ func MakeTransactionPool(ledger *ledger.Ledger, cfg config.Local) *TransactionPo cfg.TxPoolExponentialIncreaseFactor = 1 } pool := TransactionPool{ + pendingTxids: make(map[transactions.Txid]txPoolVerifyCacheVal), + rememberedTxids: make(map[transactions.Txid]txPoolVerifyCacheVal), + expiredTxCount: make(map[basics.Round]int), ledger: ledger, statusCache: makeStatusCache(cfg.TxPoolSize), logProcessBlockStats: cfg.EnableProcessBlockStats, @@ -92,8 +100,8 @@ func MakeTransactionPool(ledger *ledger.Ledger, cfg config.Local) *TransactionPo txPoolMaxSize: cfg.TxPoolSize, } pool.cond.L = &pool.mu - - pool.Reset() + pool.assemblyCond.L = &pool.assemblyMu + pool.recomputeBlockEvaluator(make(map[transactions.Txid]basics.Round)) return &pool } @@ -102,6 +110,13 @@ type txPoolVerifyCacheVal struct { params verify.Params } +type poolAsmResults struct { + ok bool + blk *ledger.ValidatedBlock + stats telemetryspec.AssembleBlockMetrics + err error +} + // TODO I moved this number to be a constant in the module, we should consider putting it in the local config const expiredHistory = 10 @@ -109,6 +124,10 @@ const expiredHistory = 10 // OnNewBlock() to process a new block that appears to be in the ledger. const timeoutOnNewBlock = time.Second +// assemblyWaitEps is the extra time AssembleBlock() waits past the +// deadline before giving up. +const assemblyWaitEps = 10 * time.Millisecond + // Reset resets the content of the transaction pool func (pool *TransactionPool) Reset() { pool.pendingTxids = make(map[transactions.Txid]txPoolVerifyCacheVal) @@ -124,15 +143,16 @@ func (pool *TransactionPool) Reset() { pool.recomputeBlockEvaluator(make(map[transactions.Txid]basics.Round)) } -// NumExpired returns the number of transactions that expired at the end of a round (only meaningful if cleanup has -// been called for that round) +// NumExpired returns the number of transactions that expired at the +// end of a round (only meaningful if cleanup has been called for that +// round). func (pool *TransactionPool) NumExpired(round basics.Round) int { pool.mu.Lock() defer pool.mu.Unlock() return pool.expiredTxCount[round] } -// PendingTxIDs return the IDs of all pending transactions +// PendingTxIDs return the IDs of all pending transactions. func (pool *TransactionPool) PendingTxIDs() []transactions.Txid { pool.pendingMu.RLock() defer pool.pendingMu.RUnlock() @@ -155,7 +175,6 @@ func (pool *TransactionPool) Pending() [][]transactions.SignedTxn { // if the underlaying array need to be expanded, the actual underlaying array would need // to be reallocated. return pool.pendingTxGroups - } // rememberCommit() saves the changes added by remember to @@ -271,25 +290,24 @@ func (pool *TransactionPool) Test(txgroup []transactions.SignedTxn) error { } type poolIngestParams struct { - checkFee bool // if set, perform fee checks - preferSync bool // if set, wait until ledger is caught up + recomputing bool // if unset, perform fee checks and wait until ledger is caught up + stats *telemetryspec.AssembleBlockMetrics } // remember attempts to add a transaction group to the pool. func (pool *TransactionPool) remember(txgroup []transactions.SignedTxn, verifyParams []verify.Params) error { params := poolIngestParams{ - checkFee: true, - preferSync: true, + recomputing: false, } return pool.ingest(txgroup, verifyParams, params) } // add tries to add the transaction group to the pool, bypassing the fee // priority checks. -func (pool *TransactionPool) add(txgroup []transactions.SignedTxn, verifyParams []verify.Params) error { +func (pool *TransactionPool) add(txgroup []transactions.SignedTxn, verifyParams []verify.Params, stats *telemetryspec.AssembleBlockMetrics) error { params := poolIngestParams{ - checkFee: false, - preferSync: false, + recomputing: true, + stats: stats, } return pool.ingest(txgroup, verifyParams, params) } @@ -304,7 +322,7 @@ func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, verifyPara return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") } - if params.preferSync { + if !params.recomputing { // Make sure that the latest block has been processed by OnNewBlock(). // If not, we might be in a race, so wait a little bit for OnNewBlock() // to catch up to the ledger. @@ -316,16 +334,14 @@ func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, verifyPara return fmt.Errorf("TransactionPool.ingest: no pending block evaluator") } } - } - if params.checkFee { err := pool.checkSufficientFee(txgroup) if err != nil { return err } } - err := pool.addToPendingBlockEvaluator(txgroup) + err := pool.addToPendingBlockEvaluator(txgroup, params.recomputing, params.stats) if err != nil { return err } @@ -339,13 +355,13 @@ func (pool *TransactionPool) ingest(txgroup []transactions.SignedTxn, verifyPara return nil } -// RememberOne stores the provided transaction +// RememberOne stores the provided transaction. // Precondition: Only RememberOne() properly-signed and well-formed transactions (i.e., ensure t.WellFormed()) func (pool *TransactionPool) RememberOne(t transactions.SignedTxn, verifyParams verify.Params) error { return pool.Remember([]transactions.SignedTxn{t}, []verify.Params{verifyParams}) } -// Remember stores the provided transaction group +// Remember stores the provided transaction group. // Precondition: Only Remember() properly-signed and well-formed transactions (i.e., ensure t.WellFormed()) func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn, verifyParams []verify.Params) error { if err := pool.checkPendingQueueSize(); err != nil { @@ -480,7 +496,7 @@ func (pool *TransactionPool) OnNewBlock(block bookkeeping.Block, delta ledger.St } } -func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactions.SignedTxn) error { +func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactions.SignedTxn, recomputing bool, stats *telemetryspec.AssembleBlockMetrics) error { r := pool.pendingBlockEvaluator.Round() + pool.numPendingWholeBlocks for _, tx := range txgroup { if tx.Txn.LastValid < r { @@ -496,15 +512,41 @@ func (pool *TransactionPool) addToPendingBlockEvaluatorOnce(txgroup []transactio for i, tx := range txgroup { txgroupad[i].SignedTxn = tx } - return pool.pendingBlockEvaluator.TransactionGroup(txgroupad) + err := pool.pendingBlockEvaluator.TransactionGroup(txgroupad) + + if recomputing { + pool.assemblyMu.Lock() + defer pool.assemblyMu.Unlock() + if !pool.assemblyResults.ok { + if err == ledger.ErrNoSpace || (pool.assemblyDeadline != time.Time{} && time.Now().After(pool.assemblyDeadline)) { + pool.assemblyResults.ok = true + + stats.StopReason = telemetryspec.AssembleBlockTimeout + if err == ledger.ErrNoSpace { + stats.StopReason = telemetryspec.AssembleBlockFull + } + pool.assemblyResults.stats = *stats + + lvb, gerr := pool.pendingBlockEvaluator.GenerateBlock() + if gerr != nil { + rnd := pool.pendingBlockEvaluator.Round() + pool.assemblyResults.err = fmt.Errorf("could not generate block for %d: %v", rnd, gerr) + } else { + pool.assemblyResults.blk = lvb + } + pool.assemblyCond.Broadcast() + } + } + } + return err } -func (pool *TransactionPool) addToPendingBlockEvaluator(txgroup []transactions.SignedTxn) error { - err := pool.addToPendingBlockEvaluatorOnce(txgroup) +func (pool *TransactionPool) addToPendingBlockEvaluator(txgroup []transactions.SignedTxn, recomputing bool, stats *telemetryspec.AssembleBlockMetrics) error { + err := pool.addToPendingBlockEvaluatorOnce(txgroup, recomputing, stats) if err == ledger.ErrNoSpace { pool.numPendingWholeBlocks++ pool.pendingBlockEvaluator.ResetTxnBytes() - err = pool.addToPendingBlockEvaluatorOnce(txgroup) + err = pool.addToPendingBlockEvaluatorOnce(txgroup, recomputing, stats) } return err } @@ -545,6 +587,10 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact pendingCount := pool.pendingCountNoLock() pool.pendingMu.RUnlock() + pool.assemblyMu.Lock() + pool.assemblyResults = poolAsmResults{} + pool.assemblyMu.Unlock() + next := bookkeeping.MakeBlock(prev) pool.numPendingWholeBlocks = 0 pool.pendingBlockEvaluator, err = pool.ledger.StartEvaluator(next.BlockHeader, pendingCount) @@ -553,159 +599,143 @@ func (pool *TransactionPool) recomputeBlockEvaluator(committedTxIds map[transact return } + var asmStats telemetryspec.AssembleBlockMetrics + asmStats.StartCount = len(txgroups) + asmStats.StopReason = telemetryspec.AssembleBlockEmpty + // Feed the transactions in order for i, txgroup := range txgroups { if len(txgroup) == 0 { + asmStats.InvalidCount++ continue } if _, alreadyCommitted := committedTxIds[txgroup[0].ID()]; alreadyCommitted { + asmStats.EarlyCommittedCount++ continue } - err := pool.add(txgroup, verifyParams[i]) + err := pool.add(txgroup, verifyParams[i], &asmStats) if err != nil { for _, tx := range txgroup { pool.statusCache.put(tx, err.Error()) } switch err.(type) { + case ledger.TransactionInLedgerError: + asmStats.CommittedCount++ + stats.RemovedInvalidCount++ case transactions.TxnDeadError: + asmStats.InvalidCount++ stats.ExpiredCount++ + case transactions.MinFeeError: + asmStats.InvalidCount++ + stats.RemovedInvalidCount++ + logging.Base().Infof("Cannot re-add pending transaction to pool: %v", err) default: + asmStats.InvalidCount++ stats.RemovedInvalidCount++ + logging.Base().Warnf("Cannot re-add pending transaction to pool: %v", err) } } } + pool.assemblyMu.Lock() + if !pool.assemblyResults.ok { + pool.assemblyResults.ok = true + pool.assemblyResults.stats = asmStats + lvb, err := pool.pendingBlockEvaluator.GenerateBlock() + if err != nil { + rnd := pool.pendingBlockEvaluator.Round() + pool.assemblyResults.err = fmt.Errorf("could not generate block for %d (end): %v", rnd, err) + } else { + pool.assemblyResults.blk = lvb + } + pool.assemblyCond.Broadcast() + } + pool.assemblyMu.Unlock() + pool.rememberCommit(true) return } // AssembleBlock assembles a block for a given round, trying not to // take longer than deadline to finish. -func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Time) (*ledger.ValidatedBlock, error) { - start := time.Now() - - pool.pendingMu.RLock() - pending := pool.pendingTxGroups - pendingCount := pool.pendingCountNoLock() - pool.pendingMu.RUnlock() - - prev, err := pool.ledger.BlockHdr(round - 1) - if err != nil { - return nil, fmt.Errorf("could not make proposals at round %d: could not read block from ledger: %v", round, err) - } - newEmptyBlk := bookkeeping.MakeBlock(prev) - - // Start the block evaluator, hinting its payset length based on the - // transaction pool's block evaluator - eval, err := pool.ledger.StartEvaluator(newEmptyBlk.BlockHeader, pendingCount) - if err != nil { - return nil, fmt.Errorf("could not make proposals at round %d: could not start evaluator: %v", round, err) - } - +func (pool *TransactionPool) AssembleBlock(round basics.Round, deadline time.Time) (assembled *ledger.ValidatedBlock, err error) { var stats telemetryspec.AssembleBlockMetrics - stats.StartCount = len(pending) - stats.StopReason = telemetryspec.AssembleBlockEmpty - // retrieve a list of all the previously known txid in the current round. We want to retrieve it here so we could avoid - // exercising the ledger read lock. - prevRoundTxIds := pool.ledger.GetRoundTxIds(round - 1) - - for len(pending) > 0 { - txgroup := pending[0] - pending = pending[1:] + if pool.logAssembleStats { + start := time.Now() + defer func() { + if err != nil { + return + } - if len(txgroup) == 0 { - stats.InvalidCount++ - continue - } + // Measure time here because we want to know how close to deadline we are + dt := time.Now().Sub(start) + stats.Nanoseconds = dt.Nanoseconds() - // if we already had this tx in the previous round, and haven't removed it yet from the txpool, that's fine. - // just skip that one. - if prevRoundTxIds[txgroup[0].ID()] { - stats.EarlyCommittedCount++ - continue - } + payset := assembled.Block().Payset + if len(payset) != 0 { + totalFees := uint64(0) - if time.Now().After(deadline) { - stats.StopReason = telemetryspec.AssembleBlockTimeout - break - } + for i, txib := range payset { + fee := txib.Txn.Fee.Raw + encodedLen := len(protocol.Encode(&txib)) - txgroupad := make([]transactions.SignedTxnWithAD, len(txgroup)) - for i, tx := range txgroup { - txgroupad[i].SignedTxn = tx - } - err := eval.TransactionGroup(txgroupad) - if err == ledger.ErrNoSpace { - stats.StopReason = telemetryspec.AssembleBlockFull - break - } - if err != nil { - // GOAL2-255: Don't warn for common case of txn already being in ledger - switch err.(type) { - case ledger.TransactionInLedgerError: - stats.CommittedCount++ - case transactions.MinFeeError: - stats.InvalidCount++ - logging.Base().Infof("Cannot add pending transaction to block: %v", err) - default: - stats.InvalidCount++ - logging.Base().Warnf("Cannot add pending transaction to block: %v", err) - } - } - } + stats.IncludedCount++ + totalFees += fee - lvb, err := eval.GenerateBlock() - if err != nil { - return nil, fmt.Errorf("could not make proposals at round %d: could not finish evaluator: %v", round, err) - } - - // Measure time here because we want to know how close to deadline we are - dt := time.Now().Sub(start) - stats.Nanoseconds = dt.Nanoseconds() - - if pool.logAssembleStats { - payset := lvb.Block().Payset - if len(payset) != 0 { - totalFees := uint64(0) - - for i, txib := range payset { - fee := txib.Txn.Fee.Raw - encodedLen := len(protocol.Encode(&txib)) - - stats.IncludedCount++ - totalFees += fee - - if i == 0 { - stats.MinFee = fee - stats.MaxFee = fee - stats.MinLength = encodedLen - stats.MaxLength = encodedLen - } else { - if fee < stats.MinFee { + if i == 0 { stats.MinFee = fee - } else if fee > stats.MaxFee { stats.MaxFee = fee - } - if encodedLen < stats.MinLength { stats.MinLength = encodedLen - } else if encodedLen > stats.MaxLength { stats.MaxLength = encodedLen + } else { + if fee < stats.MinFee { + stats.MinFee = fee + } else if fee > stats.MaxFee { + stats.MaxFee = fee + } + if encodedLen < stats.MinLength { + stats.MinLength = encodedLen + } else if encodedLen > stats.MaxLength { + stats.MaxLength = encodedLen + } } + stats.TotalLength += uint64(encodedLen) } - stats.TotalLength += uint64(encodedLen) + + stats.AverageFee = totalFees / uint64(stats.IncludedCount) } - stats.AverageFee = totalFees / uint64(stats.IncludedCount) - } + var details struct { + Round uint64 + } + details.Round = uint64(round) + logging.Base().Metrics(telemetryspec.Transaction, stats, details) + }() + } - var details struct { - Round uint64 - } - details.Round = uint64(round) - logging.Base().Metrics(telemetryspec.Transaction, stats, details) + pool.assemblyMu.Lock() + defer pool.assemblyMu.Unlock() + + pool.assemblyDeadline = deadline + deadline = deadline.Add(assemblyWaitEps) + for time.Now().Before(deadline) && (!pool.assemblyResults.ok || pool.assemblyResults.blk.Block().Round() < round) { + condvar.TimedWait(&pool.assemblyCond, deadline.Sub(time.Now())) + } + pool.assemblyDeadline = time.Time{} + + if !pool.assemblyResults.ok { + return nil, fmt.Errorf("AssembleBlock: ran out of time for round %d", round) + } + 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 { + return nil, fmt.Errorf("AssembleBlock: assembled block round does not match: %d != %d", + pool.assemblyResults.blk.Block().Round(), round) } - return lvb, nil + stats = pool.assemblyResults.stats + return pool.assemblyResults.blk, nil } diff --git a/data/transactions/application.go b/data/transactions/application.go index ee3c1aadae..da2c96c8cc 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -62,7 +62,9 @@ const ( // ClearStateOC is similar to CloseOutOC, but may never fail. This // allows users to reclaim their minimum balance from an application - // they no longer wish to opt in to. + // they no longer wish to opt in to. When an ApplicationCall + // transaction's OnCompletion is ClearStateOC, the ClearStateProgram + // executes instead of the ApprovalProgram ClearStateOC OnCompletion = 3 // UpdateApplicationOC indicates that an application transaction will @@ -80,16 +82,56 @@ const ( type ApplicationCallTxnFields struct { _struct struct{} `codec:",omitempty,omitemptyarray"` - ApplicationID basics.AppIndex `codec:"apid"` - OnCompletion OnCompletion `codec:"apan"` - ApplicationArgs [][]byte `codec:"apaa,allocbound=encodedMaxApplicationArgs"` - Accounts []basics.Address `codec:"apat,allocbound=encodedMaxAccounts"` - ForeignApps []basics.AppIndex `codec:"apfa,allocbound=encodedMaxForeignApps"` - - LocalStateSchema basics.StateSchema `codec:"apls"` + // ApplicationID is 0 when creating an application, and nonzero when + // calling an existing application. + ApplicationID basics.AppIndex `codec:"apid"` + + // OnCompletion specifies an optional side-effect that this transaction + // will have on the balance record of the sender or the application's + // creator. See the documentation for the OnCompletion type for more + // information on each possible value. + OnCompletion OnCompletion `codec:"apan"` + + // ApplicationArgs are arguments accessible to the executing + // ApprovalProgram or ClearStateProgram. + ApplicationArgs [][]byte `codec:"apaa,allocbound=encodedMaxApplicationArgs"` + + // Accounts are accounts whose balance records are accessible by the + // executing ApprovalProgram or ClearStateProgram. To access LocalState + // for an account besides the sender, that account's address must be + // listed here. + Accounts []basics.Address `codec:"apat,allocbound=encodedMaxAccounts"` + + // ForeignApps are application IDs for applications besides this one + // whose GlobalState may be read by the executing ApprovalProgram or + // ClearStateProgram. + ForeignApps []basics.AppIndex `codec:"apfa,allocbound=encodedMaxForeignApps"` + + // LocalStateSchema specifies the maximum number of each type that may + // appear in the local key/value store of users who opt in to this + // application. This field is only used during application creation + // (when the ApplicationID field is 0), + LocalStateSchema basics.StateSchema `codec:"apls"` + + // GlobalStateSchema specifies the maximum number of each type that may + // appear in the global key/value store associated with this + // application. This field is only used during application creation + // (when the ApplicationID field is 0). GlobalStateSchema basics.StateSchema `codec:"apgs"` - ApprovalProgram []byte `codec:"apap,allocbound=config.MaxAppProgramLen"` - ClearStateProgram []byte `codec:"apsu,allocbound=config.MaxAppProgramLen"` + + // ApprovalProgram is the stateful TEAL bytecode that executes on all + // ApplicationCall transactions associated with this application, + // except for those where OnCompletion is equal to ClearStateOC. If + // this program fails, the transaction is rejected. This program may + // read and write local and global state for this application. + ApprovalProgram []byte `codec:"apap,allocbound=config.MaxAppProgramLen"` + + // ClearStateProgram is the stateful TEAL bytecode that executes on + // ApplicationCall transactions associated with this application when + // OnCompletion is equal to ClearStateOC. This program will not cause + // the transaction to be rejected, even if it fails. This program may + // read and write local and global state for this application. + ClearStateProgram []byte `codec:"apsu,allocbound=config.MaxAppProgramLen"` // If you add any fields here, remember you MUST modify the Empty // method below! @@ -154,7 +196,7 @@ func cloneAppParams(m map[basics.AppIndex]basics.AppParams) map[basics.AppIndex] // if they exist. It does *not* clone the AppParams, so the returned params // must not be modified directly. func getAppParams(balances Balances, aidx basics.AppIndex) (params basics.AppParams, creator basics.Address, exists bool, err error) { - creator, exists, err = balances.GetAppCreator(aidx) + creator, exists, err = balances.GetCreator(basics.CreatableIndex(aidx), basics.AppCreatable) if err != nil { return } @@ -461,8 +503,10 @@ func (ac *ApplicationCallTxnFields) createApplication( record.AppParams[appIdx] = basics.AppParams{ ApprovalProgram: ac.ApprovalProgram, ClearStateProgram: ac.ClearStateProgram, - LocalStateSchema: ac.LocalStateSchema, - GlobalStateSchema: ac.GlobalStateSchema, + StateSchemas: basics.StateSchemas{ + LocalStateSchema: ac.LocalStateSchema, + GlobalStateSchema: ac.GlobalStateSchema, + }, } // Update the cached TotalStateSchema for this account, used @@ -473,16 +517,14 @@ func (ac *ApplicationCallTxnFields) createApplication( record.TotalAppSchema = totalSchema // Tell the cow what app we created - created := []basics.CreatableLocator{ - { - Creator: creator, - Type: basics.AppCreatable, - Index: basics.CreatableIndex(appIdx), - }, + created := &basics.CreatableLocator{ + Creator: creator, + Type: basics.AppCreatable, + Index: basics.CreatableIndex(appIdx), } // Write back to the creator's balance record and continue - err = balances.PutWithCreatables(record, created, nil) + err = balances.PutWithCreatable(record, created, nil) if err != nil { return 0, err } @@ -540,7 +582,6 @@ func (ac *ApplicationCallTxnFields) applyClearState( // Fetch the (potentially updated) sender record record, err = balances.Get(sender, false) if err != nil { - ad.EvalDelta = basics.EvalDelta{} return err } } @@ -618,17 +659,27 @@ func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec } } + // Fetch the application parameters, if they exist + params, creator, exists, err := getAppParams(balances, appIdx) + if err != nil { + return err + } + + // Ensure that the only operation we can do is ClearState if the application + // does not exist + if !exists && ac.OnCompletion != ClearStateOC { + return fmt.Errorf("only clearing out is supported for applications that do not exist") + } + // Initialize our TEAL evaluation context. Internally, this manages - // access to balance records for Stateful TEAL programs. Stateful TEAL - // may only access - // - The sender's balance record - // - The balance records of accounts explicitly listed in ac.Accounts - // - The app creator's balance record (to read/write GlobalState) - // - The balance records of creators of apps in ac.ForeignApps (to read - // GlobalState) - acctWhitelist := append(ac.Accounts, header.Sender) - appGlobalWhitelist := append(ac.ForeignApps, appIdx) - err = steva.InitLedger(balances, acctWhitelist, appGlobalWhitelist, appIdx) + // access to balance records for Stateful TEAL programs as a thin + // wrapper around Balances. + // + // Note that at this point in execution, the application might not exist + // (e.g. if it was deleted). In that case, we will pass empty + // params.StateSchemas below. This is OK because if the application is + // deleted, we will never execute its programs. + err = steva.InitLedger(balances, appIdx, params.StateSchemas) if err != nil { return err } @@ -650,18 +701,6 @@ func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec return ac.applyClearState(balances, header.Sender, appIdx, ad, steva) } - // Fetch the application parameters, if they exist - params, creator, exists, err := getAppParams(balances, appIdx) - if err != nil { - return err - } - - // Past this point, the AppParams must exist. NoOp, OptIn, CloseOut, - // Delete, and Update - if !exists { - return fmt.Errorf("only clearing out is supported for applications that do not exist") - } - // If this is an OptIn transaction, ensure that the sender has // LocalState allocated prior to TEAL execution, so that it may be // initialized in the same transaction. @@ -746,16 +785,14 @@ func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec delete(record.AppParams, appIdx) // Tell the cow what app we deleted - deleted := []basics.CreatableLocator{ - basics.CreatableLocator{ - Creator: header.Sender, - Type: basics.AppCreatable, - Index: basics.CreatableIndex(appIdx), - }, + deleted := &basics.CreatableLocator{ + Creator: creator, + Type: basics.AppCreatable, + Index: basics.CreatableIndex(appIdx), } // Write back to cow - err = balances.PutWithCreatables(record, nil, deleted) + err = balances.PutWithCreatable(record, nil, deleted) if err != nil { return err } diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 8fd48b3c40..397938af08 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -109,7 +109,7 @@ type testBalances struct { proto config.ConsensusParams put int // Put calls counter - putWith int // PutWithCreatables calls counter + putWith int // PutWithCreatable calls counter putBalances map[basics.Address]basics.AccountData putWithBalances map[basics.Address]basics.AccountData putWithNew []basics.CreatableLocator @@ -140,28 +140,32 @@ func (b *testBalances) Put(record basics.BalanceRecord) error { return nil } -func (b *testBalances) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { +func (b *testBalances) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { b.putWith++ if b.putWithBalances == nil { b.putWithBalances = make(map[basics.Address]basics.AccountData) } b.putWithBalances[record.Addr] = record.AccountData - b.putWithNew = append(b.putWithNew, newCreatables...) - b.putWithDel = append(b.putWithDel, deletedCreatables...) + if newCreatable != nil { + b.putWithNew = append(b.putWithNew, *newCreatable) + } + if deletedCreatable != nil { + b.putWithDel = append(b.putWithDel, *deletedCreatable) + } return nil } -func (b *testBalances) GetAssetCreator(aidx basics.AssetIndex) (basics.Address, error) { - return basics.Address{}, nil -} +func (b *testBalances) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + if ctype == basics.AppCreatable { + aidx := basics.AppIndex(cidx) + if aidx == appIdxError { // magic for test + return basics.Address{}, false, fmt.Errorf("mock synthetic error") + } -func (b *testBalances) GetAppCreator(aidx basics.AppIndex) (basics.Address, bool, error) { - if aidx == appIdxError { // magic for test - return basics.Address{}, false, fmt.Errorf("mock synthetic error") + creator, ok := b.appCreators[aidx] + return creator, ok, nil } - - creator, ok := b.appCreators[aidx] - return creator, ok, nil + return basics.Address{}, false, nil } func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { @@ -184,7 +188,7 @@ func (b *testBalancesPass) Put(record basics.BalanceRecord) error { return nil } -func (b *testBalancesPass) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { +func (b *testBalancesPass) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { if b.balances == nil { b.balances = make(map[basics.Address]basics.AccountData) } @@ -196,7 +200,7 @@ func (b *testBalances) ConsensusParams() config.ConsensusParams { return b.proto } -// ResetWrites clears side effects of Put/PutWithCreatables +// ResetWrites clears side effects of Put/PutWithCreatable func (b *testBalances) ResetWrites() { b.put = 0 b.putWith = 0 @@ -211,12 +215,9 @@ func (b *testBalances) SetProto(name protocol.ConsensusVersion) { } type testEvaluator struct { - pass bool - delta basics.EvalDelta - - acctWhitelist []basics.Address - appGlobalWhitelist []basics.AppIndex - appIdx basics.AppIndex + pass bool + delta basics.EvalDelta + appIdx basics.AppIndex } // Eval for tests that fail on program version > 10 and returns pass/delta from its own state rather than running the program @@ -235,10 +236,8 @@ func (e *testEvaluator) Check(program []byte) (cost int, err error) { return len(program), nil } -func (e *testEvaluator) InitLedger(balances Balances, acctWhitelist []basics.Address, appGlobalWhitelist []basics.AppIndex, appIdx basics.AppIndex) error { +func (e *testEvaluator) InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { e.appIdx = appIdx - e.acctWhitelist = acctWhitelist - e.appGlobalWhitelist = appGlobalWhitelist return nil } @@ -489,8 +488,8 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { a.Equal(0, b.put) a.Equal(0, b.putWith) - // simulate balances.GetAppCreator and balances.Get get out of sync - // creator received from balances.GetAppCreator and has app params + // simulate balances.GetCreator and balances.Get get out of sync + // creator received from balances.GetCreator and has app params // and its balances.Get record is out of sync/not initialized // ensure even if AppParams were allocated they are empty creator = getRandomAddress(a) @@ -767,32 +766,34 @@ func TestAppCallApplyCreate(t *testing.T) { b.SetProto(protocol.ConsensusFuture) - // error on non-existing app + // this test will succeed in creating the app, but then fail + // because the mock balances doesn't update the creators table + // so it will think the app doesn't exist err = ac.apply(h, &b, spec, ad, txnCounter, &steva) a.Error(err) a.Contains(err.Error(), "applications that do not exist") - a.Equal(txnCounter+1, uint64(steva.appIdx)) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) a.Equal(0, b.put) a.Equal(1, b.putWith) - b.appCreators = map[basics.AppIndex]basics.Address{steva.appIdx: creator} + createdAppIdx := basics.AppIndex(txnCounter + 1) + b.appCreators = map[basics.AppIndex]basics.Address{createdAppIdx: creator} + + // save the created app info to the side saved := b.putWithBalances[creator] b.ResetWrites() + // now looking up the creator will succeed, but we reset writes, so + // they won't have the app params err = ac.apply(h, &b, spec, ad, txnCounter, &steva) a.Error(err) - a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", steva.appIdx)) - a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) + a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", createdAppIdx)) a.Equal(0, b.put) a.Equal(1, b.putWith) b.ResetWrites() + // now we give the creator the app params again cp := basics.AccountData{} cp.AppParams = cloneAppParams(saved.AppParams) cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) @@ -801,8 +802,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "transaction rejected by ApprovalProgram") a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) a.Equal(0, b.put) a.Equal(1, b.putWith) // ensure original balance record in the mock was not changed @@ -826,8 +825,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "cannot apply GlobalState delta") a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) a.Equal(0, b.put) a.Equal(1, b.putWith) a.Equal(basics.EvalDelta{}, ad.EvalDelta) @@ -847,8 +844,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.Error(err) a.Contains(err.Error(), "too much space: store integer") a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{steva.appIdx}, steva.appGlobalWhitelist) a.Equal(0, b.put) a.Equal(1, b.putWith) a.Equal(saved, b.balances[creator]) @@ -866,8 +861,6 @@ func TestAppCallApplyCreate(t *testing.T) { a.NoError(err) appIdx := steva.appIdx a.Equal(uint64(appIdx), txnCounter+1) - a.Equal([]basics.Address{sender}, steva.acctWhitelist) - a.Equal([]basics.AppIndex{appIdx}, steva.appGlobalWhitelist) a.Equal(1, b.put) a.Equal(1, b.putWith) a.Equal(saved, b.balances[creator]) @@ -1108,8 +1101,10 @@ func TestAppCallApplyCloseOut(t *testing.T) { OnCompletion: CloseOutOC, } params := basics.AppParams{ - ApprovalProgram: []byte{1}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, } h := Header{ Sender: sender, @@ -1190,8 +1185,10 @@ func TestAppCallApplyUpdate(t *testing.T) { ClearStateProgram: []byte{3}, } params := basics.AppParams{ - ApprovalProgram: []byte{1}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, } h := Header{ Sender: sender, @@ -1252,8 +1249,10 @@ func TestAppCallApplyDelete(t *testing.T) { OnCompletion: DeleteApplicationOC, } params := basics.AppParams{ - ApprovalProgram: []byte{1}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, } h := Header{ Sender: sender, diff --git a/data/transactions/asset.go b/data/transactions/asset.go index ce998c9541..13f81ef1c9 100644 --- a/data/transactions/asset.go +++ b/data/transactions/asset.go @@ -78,7 +78,7 @@ type AssetFreezeTxnFields struct { AssetFrozen bool `codec:"afrz"` } -func clone(m map[basics.AssetIndex]basics.AssetHolding) map[basics.AssetIndex]basics.AssetHolding { +func cloneAssetHoldings(m map[basics.AssetIndex]basics.AssetHolding) map[basics.AssetIndex]basics.AssetHolding { res := make(map[basics.AssetIndex]basics.AssetHolding) for id, val := range m { res[id] = val @@ -86,7 +86,7 @@ func clone(m map[basics.AssetIndex]basics.AssetHolding) map[basics.AssetIndex]ba return res } -func cloneParams(m map[basics.AssetIndex]basics.AssetParams) map[basics.AssetIndex]basics.AssetParams { +func cloneAssetParams(m map[basics.AssetIndex]basics.AssetParams) map[basics.AssetIndex]basics.AssetParams { res := make(map[basics.AssetIndex]basics.AssetParams) for id, val := range m { res[id] = val @@ -95,11 +95,18 @@ func cloneParams(m map[basics.AssetIndex]basics.AssetParams) map[basics.AssetInd } func getParams(balances Balances, aidx basics.AssetIndex) (params basics.AssetParams, creator basics.Address, err error) { - creator, err = balances.GetAssetCreator(aidx) + creator, exists, err := balances.GetCreator(basics.CreatableIndex(aidx), basics.AssetCreatable) if err != nil { return } + // For assets, anywhere we're attempting to fetch parameters, we are + // assuming that the asset should exist. + if !exists { + err = fmt.Errorf("asset %d does not exist or has been deleted", aidx) + return + } + creatorRecord, err := balances.Get(creator, false) if err != nil { return @@ -121,8 +128,8 @@ func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec Spec if err != nil { return err } - record.Assets = clone(record.Assets) - record.AssetParams = cloneParams(record.AssetParams) + record.Assets = cloneAssetHoldings(record.Assets) + record.AssetParams = cloneAssetParams(record.AssetParams) // Ensure index is never zero newidx := basics.AssetIndex(txnCounter + 1) @@ -142,7 +149,14 @@ func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec Spec return fmt.Errorf("too many assets in account: %d > %d", len(record.Assets), balances.ConsensusParams().MaxAssetsPerAccount) } - return balances.Put(record) + // Tell the cow what asset we created + created := &basics.CreatableLocator{ + Creator: header.Sender, + Type: basics.AssetCreatable, + Index: basics.CreatableIndex(newidx), + } + + return balances.PutWithCreatable(record, created, nil) } // Re-configuration and destroying must be done by the manager key. @@ -160,9 +174,10 @@ func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec Spec return err } - record.Assets = clone(record.Assets) - record.AssetParams = cloneParams(record.AssetParams) + 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. @@ -170,6 +185,13 @@ func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec Spec return fmt.Errorf("cannot destroy asset: creator is holding only %d/%d", record.Assets[cc.ConfigAsset].Amount, params.Total) } + // Tell the cow what asset we deleted + deleted = &basics.CreatableLocator{ + Creator: creator, + Type: basics.AssetCreatable, + Index: basics.CreatableIndex(cc.ConfigAsset), + } + delete(record.Assets, cc.ConfigAsset) delete(record.AssetParams, cc.ConfigAsset) } else { @@ -190,7 +212,7 @@ func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec Spec record.AssetParams[cc.ConfigAsset] = params } - return balances.Put(record) + return balances.PutWithCreatable(record, nil, deleted) } func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { @@ -203,7 +225,7 @@ func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, am return err } - snd.Assets = clone(snd.Assets) + snd.Assets = cloneAssetHoldings(snd.Assets) sndHolding, ok := snd.Assets[asset] if !ok { return fmt.Errorf("asset %v missing from %v", asset, addr) @@ -233,7 +255,7 @@ func putIn(balances Balances, addr basics.Address, asset basics.AssetIndex, amou return err } - rcv.Assets = clone(rcv.Assets) + rcv.Assets = cloneAssetHoldings(rcv.Assets) rcvHolding, ok := rcv.Assets[asset] if !ok { return fmt.Errorf("asset %v missing from %v", asset, addr) @@ -283,7 +305,7 @@ func (ct AssetTransferTxnFields) apply(header Header, balances Balances, spec Sp return err } - snd.Assets = clone(snd.Assets) + snd.Assets = cloneAssetHoldings(snd.Assets) sndHolding, ok := snd.Assets[ct.XferAsset] if !ok { // Initialize holding with default Frozen value. @@ -379,7 +401,7 @@ func (ct AssetTransferTxnFields) apply(header Header, balances Balances, spec Sp return err } - snd.Assets = clone(snd.Assets) + snd.Assets = cloneAssetHoldings(snd.Assets) sndHolding = snd.Assets[ct.XferAsset] if sndHolding.Amount != 0 { return fmt.Errorf("asset %v not zero (%d) after closing", ct.XferAsset, sndHolding.Amount) @@ -411,7 +433,7 @@ func (cf AssetFreezeTxnFields) apply(header Header, balances Balances, spec Spec if err != nil { return err } - record.Assets = clone(record.Assets) + record.Assets = cloneAssetHoldings(record.Assets) holding, ok := record.Assets[cf.FreezeAsset] if !ok { diff --git a/data/transactions/keyreg_test.go b/data/transactions/keyreg_test.go index 012be2e216..1adb5038a3 100644 --- a/data/transactions/keyreg_test.go +++ b/data/transactions/keyreg_test.go @@ -38,11 +38,7 @@ func (balances keyregTestBalances) Get(addr basics.Address, withPendingRewards b return balances.addrs[addr], nil } -func (balances keyregTestBalances) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { - return basics.Address{}, nil -} - -func (balances keyregTestBalances) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { +func (balances keyregTestBalances) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { return basics.Address{}, true, nil } @@ -50,7 +46,7 @@ func (balances keyregTestBalances) Put(basics.BalanceRecord) error { return nil } -func (balances keyregTestBalances) PutWithCreatables(basics.BalanceRecord, []basics.CreatableLocator, []basics.CreatableLocator) error { +func (balances keyregTestBalances) PutWithCreatable(basics.BalanceRecord, *basics.CreatableLocator, *basics.CreatableLocator) error { return nil } diff --git a/data/transactions/logic/Makefile b/data/transactions/logic/Makefile index 8895bc4697..62c164dc62 100644 --- a/data/transactions/logic/Makefile +++ b/data/transactions/logic/Makefile @@ -1,7 +1,7 @@ all: TEAL_opcodes.md wat.md fields_string.go TEAL_opcodes.md: fields_string.go ../../../cmd/opdoc/opdoc.go eval.go assembler.go doc.go opcodes.go - go run ../../../cmd/opdoc/opdoc.go + go run ../../../cmd/opdoc/opdoc.go ../../../cmd/opdoc/tmLanguage.go fields_string.go: fields.go go generate diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 0086579279..4fb4f9a8d6 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -127,6 +127,7 @@ For two-argument ops, `A` is the previous element on the stack and `B` is the la | `^` | A bitwise-xor B | | `~` | bitwise invert value X | | `mulw` | A times B out to 128-bit long result as low (top) and high uint64 values on the stack | +| `addw` | A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack | | `concat` | pop two byte strings A and B and join them, push the result | | `substring` | pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result | | `substring3` | pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result | @@ -200,6 +201,22 @@ Some of these have immediate data in the byte or bytes after the opcode. | 29 | NumAccounts | uint64 | Number of Accounts. LogicSigVersion >= 2. | | 30 | ApprovalProgram | []byte | Approval program. LogicSigVersion >= 2. | | 31 | ClearStateProgram | []byte | Clear state program. LogicSigVersion >= 2. | +| 32 | RekeyTo | []byte | 32 byte Sender's new AuthAddr. LogicSigVersion >= 2. | +| 33 | ConfigAsset | uint64 | Asset ID in asset config transaction. LogicSigVersion >= 2. | +| 34 | ConfigAssetTotal | uint64 | Total number of units of this asset created. LogicSigVersion >= 2. | +| 35 | ConfigAssetDecimals | uint64 | Number of digits to display after the decimal place when displaying the asset. LogicSigVersion >= 2. | +| 36 | ConfigAssetDefaultFrozen | uint64 | Whether the asset's slots are frozen by default or not, 0 or 1. LogicSigVersion >= 2. | +| 37 | ConfigAssetUnitName | []byte | Unit name of the asset. LogicSigVersion >= 2. | +| 38 | ConfigAssetName | []byte | The asset name. LogicSigVersion >= 2. | +| 39 | ConfigAssetURL | []byte | URL. LogicSigVersion >= 2. | +| 40 | ConfigAssetMetadataHash | []byte | 32 byte commitment to some unspecified asset metadata. LogicSigVersion >= 2. | +| 41 | ConfigAssetManager | []byte | 32 byte address. LogicSigVersion >= 2. | +| 42 | ConfigAssetReserve | []byte | 32 byte address. LogicSigVersion >= 2. | +| 43 | ConfigAssetFreeze | []byte | 32 byte address. LogicSigVersion >= 2. | +| 44 | ConfigAssetClawback | []byte | 32 byte address. LogicSigVersion >= 2. | +| 45 | FreezeAsset | uint64 | Asset ID being frozen or un-frozen. LogicSigVersion >= 2. | +| 46 | FreezeAssetAccount | []byte | 32 byte address of the account whose asset slot is being frozen or un-frozen. LogicSigVersion >= 2. | +| 47 | FreezeAssetFrozen | uint64 | The new frozen value, 0 or 1. LogicSigVersion >= 2. | Additional details in the [opcodes document](TEAL_opcodes.md#txn) on the `txn` op. @@ -218,6 +235,7 @@ Global fields are fields that are common to all the transactions in the group. I | 5 | LogicSigVersion | uint64 | Maximum supported TEAL version. LogicSigVersion >= 2. | | 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | | 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | +| 8 | CurrentApplicationID | uint64 | ID of current application executing. Fails if no such application is executing. LogicSigVersion >= 2. | **Asset Fields** @@ -236,7 +254,7 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as | 1 | AssetDecimals | uint64 | See AssetParams.Decimals | | 2 | AssetDefaultFrozen | uint64 | Frozen by default or not | | 3 | AssetUnitName | []byte | Asset unit name | -| 4 | AssetAssetName | []byte | Asset name | +| 4 | AssetName | []byte | Asset name | | 5 | AssetURL | []byte | URL with additional info about the asset | | 6 | AssetMetadataHash | []byte | Arbitrary commitment | | 7 | AssetManager | []byte | Manager commitment | @@ -262,12 +280,12 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as | Op | Description | | --- | --- | -| `balance` | get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction | +| `balance` | get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender | | `app_opted_in` | check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1} | | `app_local_get` | read from account specified by Txn.Accounts[A] from local state of the current application key B => value | | `app_local_get_ex` | read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value} | | `app_global_get` | read key A from global state of a current application => value | -| `app_global_get_ex` | read from application A global state key B => {0 or 1 (top), value} | +| `app_global_get_ex` | read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app | | `app_local_put` | write to account specified by Txn.Accounts[A] to local state of a current application key B with value C | | `app_global_put` | write key A and value B to global state of the current application | | `app_local_del` | delete from account specified by Txn.Accounts[A] local state key B of the current application | @@ -279,6 +297,9 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as The assembler parses line by line. Ops that just use the stack appear on a line by themselves. Ops that take arguments are the op and then whitespace and then any argument or arguments. +The first line may contain a special version pragma `#pragma version X`. +By default the assembler generates TEAL v1. So that all TEAL v2 programs must start with `#pragma version 2` + "`//`" prefixes a line comment. ## Constants and Pseudo-Ops @@ -334,9 +355,9 @@ A '[proto-buf style variable length unsigned int](https://developers.google.com/ Current design and implementation limitations to be aware of. * TEAL cannot create or change a transaction, only approve or reject. -* TEAL cannot lookup balances of Algos or other assets. (Standard transaction accounting will apply after TEAL has run and authorized a transaction. A TEAL-approved transaction could still be invalid by other accounting rules just as a standard signed transaction could be invalid. e.g. I can't give away money I don't have.) +* Stateless TEAL cannot lookup balances of Algos or other assets. (Standard transaction accounting will apply after TEAL has run and authorized a transaction. A TEAL-approved transaction could still be invalid by other accounting rules just as a standard signed transaction could be invalid. e.g. I can't give away money I don't have.) * TEAL cannot access information in previous blocks. TEAL cannot access most information in other transactions in the current block. (TEAL can access fields of the transaction it is attached to and the transactions in an atomic transaction group.) * TEAL cannot know exactly what round the current transaction will commit in (but it is somewhere in FirstValid through LastValid). * TEAL cannot know exactly what time its transaction is committed. -* TEAL cannot loop. Its branch instruction `bnz` "branch if not zero" can only branch forward so as to skip some code. +* TEAL cannot loop. Its branch instructions `bnz` "branch if not zero", `bz` "branch if zero" and `b` "branch" can only branch forward so as to skip some code. * TEAL cannot recurse. There is no subroutine jump operation. diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md index 5a58cb820c..d4e0db8e21 100644 --- a/data/transactions/logic/README_in.md +++ b/data/transactions/logic/README_in.md @@ -121,6 +121,9 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as The assembler parses line by line. Ops that just use the stack appear on a line by themselves. Ops that take arguments are the op and then whitespace and then any argument or arguments. +The first line may contain a special version pragma `#pragma version X`. +By default the assembler generates TEAL v1. So that all TEAL v2 programs must start with `#pragma version 2` + "`//`" prefixes a line comment. ## Constants and Pseudo-Ops @@ -176,9 +179,9 @@ A '[proto-buf style variable length unsigned int](https://developers.google.com/ Current design and implementation limitations to be aware of. * TEAL cannot create or change a transaction, only approve or reject. -* TEAL cannot lookup balances of Algos or other assets. (Standard transaction accounting will apply after TEAL has run and authorized a transaction. A TEAL-approved transaction could still be invalid by other accounting rules just as a standard signed transaction could be invalid. e.g. I can't give away money I don't have.) +* Stateless TEAL cannot lookup balances of Algos or other assets. (Standard transaction accounting will apply after TEAL has run and authorized a transaction. A TEAL-approved transaction could still be invalid by other accounting rules just as a standard signed transaction could be invalid. e.g. I can't give away money I don't have.) * TEAL cannot access information in previous blocks. TEAL cannot access most information in other transactions in the current block. (TEAL can access fields of the transaction it is attached to and the transactions in an atomic transaction group.) * TEAL cannot know exactly what round the current transaction will commit in (but it is somewhere in FirstValid through LastValid). * TEAL cannot know exactly what time its transaction is committed. -* TEAL cannot loop. Its branch instruction `bnz` "branch if not zero" can only branch forward so as to skip some code. +* TEAL cannot loop. Its branch instructions `bnz` "branch if not zero", `bz` "branch if zero" and `b` "branch" can only branch forward so as to skip some code. * TEAL cannot recurse. There is no subroutine jump operation. diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 928d39e70c..d1d81b820f 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -58,6 +58,8 @@ The 32 byte public key is the last element on the stack, preceded by the 64 byte - Pushes: uint64 - A plus B. Panic on overflow. +Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `addw`. + ## - - Opcode: 0x09 @@ -209,6 +211,14 @@ Overflow is an error condition which halts execution and fails the transaction. - Pushes: uint64, uint64 - A times B out to 128-bit long result as low (top) and high uint64 values on the stack +## addw + +- Opcode: 0x1e +- Pops: *... stack*, {uint64 A}, {uint64 B} +- Pushes: uint64, uint64 +- A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack +- LogicSigVersion >= 2 + ## intcblock - Opcode: 0x20 {varuint length} [{varuint value}, ...] @@ -380,6 +390,22 @@ Overflow is an error condition which halts execution and fails the transaction. | 29 | NumAccounts | uint64 | Number of Accounts. LogicSigVersion >= 2. | | 30 | ApprovalProgram | []byte | Approval program. LogicSigVersion >= 2. | | 31 | ClearStateProgram | []byte | Clear state program. LogicSigVersion >= 2. | +| 32 | RekeyTo | []byte | 32 byte Sender's new AuthAddr. LogicSigVersion >= 2. | +| 33 | ConfigAsset | uint64 | Asset ID in asset config transaction. LogicSigVersion >= 2. | +| 34 | ConfigAssetTotal | uint64 | Total number of units of this asset created. LogicSigVersion >= 2. | +| 35 | ConfigAssetDecimals | uint64 | Number of digits to display after the decimal place when displaying the asset. LogicSigVersion >= 2. | +| 36 | ConfigAssetDefaultFrozen | uint64 | Whether the asset's slots are frozen by default or not, 0 or 1. LogicSigVersion >= 2. | +| 37 | ConfigAssetUnitName | []byte | Unit name of the asset. LogicSigVersion >= 2. | +| 38 | ConfigAssetName | []byte | The asset name. LogicSigVersion >= 2. | +| 39 | ConfigAssetURL | []byte | URL. LogicSigVersion >= 2. | +| 40 | ConfigAssetMetadataHash | []byte | 32 byte commitment to some unspecified asset metadata. LogicSigVersion >= 2. | +| 41 | ConfigAssetManager | []byte | 32 byte address. LogicSigVersion >= 2. | +| 42 | ConfigAssetReserve | []byte | 32 byte address. LogicSigVersion >= 2. | +| 43 | ConfigAssetFreeze | []byte | 32 byte address. LogicSigVersion >= 2. | +| 44 | ConfigAssetClawback | []byte | 32 byte address. LogicSigVersion >= 2. | +| 45 | FreezeAsset | uint64 | Asset ID being frozen or un-frozen. LogicSigVersion >= 2. | +| 46 | FreezeAssetAccount | []byte | 32 byte address of the account whose asset slot is being frozen or un-frozen. LogicSigVersion >= 2. | +| 47 | FreezeAssetFrozen | uint64 | The new frozen value, 0 or 1. LogicSigVersion >= 2. | TypeEnum mapping: @@ -416,6 +442,7 @@ FirstValidTime causes the program to fail. The field is reserved for future use. | 5 | LogicSigVersion | uint64 | Maximum supported TEAL version. LogicSigVersion >= 2. | | 6 | Round | uint64 | Current round number. LogicSigVersion >= 2. | | 7 | LatestTimestamp | uint64 | Last confirmed block UNIX timestamp. Fails if negative. LogicSigVersion >= 2. | +| 8 | CurrentApplicationID | uint64 | ID of current application executing. Fails if no such application is executing. LogicSigVersion >= 2. | ## gtxn @@ -549,7 +576,7 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - Opcode: 0x60 - Pops: *... stack*, uint64 - Pushes: uint64 -- get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction +- get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender - LogicSigVersion >= 2 - Mode: Application @@ -602,11 +629,11 @@ params: state key. Return: value. The value is zero if the key does not exist. - Opcode: 0x65 - Pops: *... stack*, {uint64 A}, {[]byte B} - Pushes: uint64, any -- read from application A global state key B => {0 or 1 (top), value} +- read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app - LogicSigVersion >= 2 - Mode: Application -params: application id, state key. Return: value. +params: application index, state key. Return: value. Application index is ## app_local_put @@ -686,7 +713,7 @@ params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherw | 1 | AssetDecimals | uint64 | See AssetParams.Decimals | | 2 | AssetDefaultFrozen | uint64 | Frozen by default or not | | 3 | AssetUnitName | []byte | Asset unit name | -| 4 | AssetAssetName | []byte | Asset name | +| 4 | AssetName | []byte | Asset name | | 5 | AssetURL | []byte | URL with additional info about the asset | | 6 | AssetMetadataHash | []byte | Arbitrary commitment | | 7 | AssetManager | []byte | Manager commitment | diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 989f7b91cb..86e8faec3e 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -704,9 +704,12 @@ func assembleTxn(ops *OpStream, spec *OpSpec, args []string) error { return errors.New("txn expects one argument") } fs, ok := txnFieldSpecByName[args[0]] - if !ok || fs.version > ops.Version { + if !ok { return fmt.Errorf("txn unknown arg %s", args[0]) } + if fs.version > ops.Version { + return fmt.Errorf("txn %s available in version %d. Missed #pragma version?", args[0], fs.version) + } val := fs.field return ops.Txn(uint64(val)) } @@ -727,9 +730,16 @@ func assembleTxna(ops *OpStream, spec *OpSpec, args []string) error { return errors.New("txna expects two arguments") } fs, ok := txnFieldSpecByName[args[0]] - if !ok || fs.version > ops.Version || fs.field != ApplicationArgs && fs.field != Accounts { + if !ok { + return fmt.Errorf("txna unknown arg %s", args[0]) + } + _, ok = txnaFieldSpecByField[fs.field] + if !ok { return fmt.Errorf("txna unknown arg %s", args[0]) } + if fs.version > ops.Version { + return fmt.Errorf("txna %s available in version %d. Missed #pragma version?", args[0], fs.version) + } arrayFieldIdx, err := strconv.ParseUint(args[1], 0, 64) if err != nil { return err @@ -747,8 +757,11 @@ func assembleGtxn(ops *OpStream, spec *OpSpec, args []string) error { return err } fs, ok := txnFieldSpecByName[args[1]] - if !ok || fs.version > ops.Version { - return fmt.Errorf("gtxn unknown arg %s", args[0]) + if !ok { + return fmt.Errorf("gtxn unknown arg %s", args[1]) + } + if fs.version > ops.Version { + return fmt.Errorf("gtxn %s available in version %d. Missed #pragma version?", args[1], fs.version) } val := fs.field return ops.Gtxn(gtid, uint64(val)) @@ -773,8 +786,15 @@ func assembleGtxna(ops *OpStream, spec *OpSpec, args []string) error { return err } fs, ok := txnFieldSpecByName[args[1]] - if !ok || fs.version > ops.Version || fs.field != ApplicationArgs && fs.field != Accounts { - return fmt.Errorf("gtxna unknown arg %s", args[0]) + if !ok { + return fmt.Errorf("gtxna unknown arg %s", args[1]) + } + _, ok = txnaFieldSpecByField[fs.field] + if !ok { + return fmt.Errorf("gtxna unknown arg %s", args[1]) + } + if fs.version > ops.Version { + return fmt.Errorf("gtxna %s available in version %d. Missed #pragma version?", args[1], fs.version) } arrayFieldIdx, err := strconv.ParseUint(args[2], 0, 64) if err != nil { @@ -789,9 +809,12 @@ func assembleGlobal(ops *OpStream, spec *OpSpec, args []string) error { return errors.New("global expects one argument") } fs, ok := globalFieldSpecByName[args[0]] - if !ok || fs.version > ops.Version { + if !ok { return fmt.Errorf("global unknown arg %v", args[0]) } + if fs.version > ops.Version { + return fmt.Errorf("global %s available in version %d. Missed #pragma version?", args[0], fs.version) + } val := fs.gfield return ops.Global(uint64(val)) } @@ -984,8 +1007,8 @@ func (ops *OpStream) checkArgs(spec OpSpec) error { return nil } -// Assemble reads text from an input and accumulates the program -func (ops *OpStream) Assemble(fin io.Reader) error { +// assemble reads text from an input and accumulates the program +func (ops *OpStream) assemble(fin io.Reader) error { scanner := bufio.NewScanner(fin) ops.sourceLine = 0 for scanner.Scan() { @@ -1073,7 +1096,13 @@ func (ops *OpStream) resolveLabels() (err error) { } // AssemblerDefaultVersion what version of code do we emit by default -const AssemblerDefaultVersion = LogicVersion +// AssemblerDefaultVersion is set to 1 on puprose +// to prevent accidental building of v1 official templates with version 2 +// because these templates are not aware of rekeying. +const AssemblerDefaultVersion = 1 + +// AssemblerMaxVersion is a maximum supported assembler version +const AssemblerMaxVersion = LogicVersion const assemblerNoVersion = (^uint64(0)) // Bytes returns the finished program bytes @@ -1153,7 +1182,7 @@ func AssembleStringWithVersion(text string, version uint64) ([]byte, error) { // AssembleStringWithVersionEx takes an entire program in a string and assembles it to bytecode // using the assembler version specified. -// If version is zero it uses #pragma version or fallbacks to AssemblerDefaultVersion. +// If version is assemblerNoVersion it uses #pragma version or fallbacks to AssemblerDefaultVersion. // It also returns PC to source line mapping. func AssembleStringWithVersionEx(text string, version uint64) ([]byte, map[int]int, error) { sr := strings.NewReader(text) @@ -1180,7 +1209,7 @@ func AssembleStringWithVersionEx(text string, version uint64) ([]byte, map[int]i sr = strings.NewReader(text) ops := OpStream{Version: version} - err = ops.Assemble(sr) + err = ops.assemble(sr) if err != nil { return nil, nil, err } @@ -1230,7 +1259,7 @@ func (ps *PragmaStream) Process(fin io.Reader) (err error) { if err != nil { return } - if ver < 1 || ver > AssemblerDefaultVersion { + if ver < 1 || ver > AssemblerMaxVersion { err = fmt.Errorf("unsupported version: %d", ver) return } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index b350d2801b..d056c1af67 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -194,13 +194,33 @@ txn NumAppArgs txn NumAccounts txn ApprovalProgram txn ClearStateProgram +txn RekeyTo +int 0 +int 1 +addw +txn ConfigAsset +txn ConfigAssetTotal +txn ConfigAssetDecimals +txn ConfigAssetDefaultFrozen +txn ConfigAssetUnitName +txn ConfigAssetName +txn ConfigAssetURL +txn ConfigAssetMetadataHash +txn ConfigAssetManager +txn ConfigAssetReserve +txn ConfigAssetFreeze +txn ConfigAssetClawback +txn FreezeAsset +txn FreezeAssetAccount +txn FreezeAssetFrozen ` // Check that assembly output is stable across time. func TestAssemble(t *testing.T) { // UPDATE PROCEDURE: // Run test. It should pass. If test is not passing, do not change this test, fix the assembler first. - // Extend this test program text. It is preferrable to append instructions to the end so that the program byte hex is visually similar and also simply extended by some new bytes. + // Extend this test program text. Append instructions to the end so that the program byte hex is visually similar and also simply extended by some new bytes, + // and so that version-dependent tests pass. // Copy hex string from failing test output into source. // Run test. It should pass. // @@ -216,10 +236,10 @@ func TestAssemble(t *testing.T) { t.Errorf("test should contain op %v", spec.Name) } } - program, err := AssembleString(bigTestAssembleNonsenseProgram) + program, err := AssembleStringWithVersion(bigTestAssembleNonsenseProgram, AssemblerMaxVersion) require.NoError(t, err) // check that compilation is stable over time and we assemble to the same bytes this month that we did last month. - expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b6921072105700048482107210571004848361c0037001a0031183119311b311d311e311f") + expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b6921072105700048482107210571004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f") if bytes.Compare(expectedBytes, program) != 0 { // this print is for convenience if the program has been changed. the hex string can be copy pasted back in as a new expected result. t.Log(hex.EncodeToString(program)) @@ -234,7 +254,7 @@ pop gtxn 0 ApplicationArgs 0 // alias to gtxn pop ` - prog1, err := AssembleString(source1) + prog1, err := AssembleStringWithVersion(source1, AssemblerMaxVersion) require.NoError(t, err) source2 := `txna Accounts 0 @@ -242,7 +262,7 @@ pop gtxna 0 ApplicationArgs 0 pop ` - prog2, err := AssembleString(source2) + prog2, err := AssembleStringWithVersion(source2, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, prog1, prog2) @@ -250,37 +270,37 @@ pop func TestAssembleTxna(t *testing.T) { source := `txna Accounts 256` - _, err := AssembleString(source) + _, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "txna cannot look up beyond index 255") source = `txna ApplicationArgs 256` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "txna cannot look up beyond index 255") source = `txna Sender 256` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "txna unknown arg") source = `gtxna 0 Accounts 256` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") source = `gtxna 0 ApplicationArgs 256` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") source = `gtxna 256 Accounts 0` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "gtxna cannot look up beyond index 255") source = `gtxna 0 Sender 256` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "gtxna unknown arg") @@ -295,12 +315,12 @@ func TestAssembleTxna(t *testing.T) { require.Contains(t, err.Error(), "txn expects one or two arguments") source = `txna Accounts 0 1` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "txna expects two arguments") source = `txna Accounts a` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "strconv.ParseUint") @@ -315,17 +335,17 @@ func TestAssembleTxna(t *testing.T) { require.Contains(t, err.Error(), "gtxn expects two or three arguments") source = `gtxna 0 Accounts 1 2` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "gtxna expects three arguments") source = `gtxna a Accounts 0` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "strconv.ParseUint") source = `gtxna 0 Accounts a` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "strconv.ParseUint") @@ -347,12 +367,12 @@ func TestAssembleTxna(t *testing.T) { func TestAssembleGlobal(t *testing.T) { source := `global` - _, err := AssembleString(source) + _, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "global expects one argument") source = `global a` - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "global unknown arg") } @@ -363,7 +383,7 @@ int 1 + // comment ` - _, err := AssembleString(source) + _, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "wanted type uint64 got []byte") } @@ -374,7 +394,7 @@ func mutateProgVersion(version uint64, prog string) string { } func TestOpUint(t *testing.T) { - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { ops := OpStream{Version: v} err := ops.Uint(0xcafebabe) @@ -390,7 +410,7 @@ func TestOpUint(t *testing.T) { func TestOpUint64(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { t.Parallel() ops := OpStream{Version: v} @@ -406,7 +426,7 @@ func TestOpUint64(t *testing.T) { func TestOpBytes(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { ops := OpStream{Version: v} err := ops.ByteLiteral([]byte("abcdef")) @@ -421,7 +441,7 @@ func TestOpBytes(t *testing.T) { func TestAssembleInt(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { text := "int 0xcafebabe" program, err := AssembleStringWithVersion(text, v) @@ -461,7 +481,7 @@ func TestAssembleBytes(t *testing.T) { `byte "\x61\x62\x63\x64\x65\x66"`, `byte "abcdef"`, } - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { for _, vi := range variations { program, err := AssembleStringWithVersion(vi, v) @@ -475,7 +495,7 @@ func TestAssembleBytes(t *testing.T) { } func TestAssembleBytesString(t *testing.T) { - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { text := `byte "foo bar"` _, err := AssembleStringWithVersion(text, v) @@ -694,7 +714,7 @@ func TestAssembleRejectNegJump(t *testing.T) { text := `wat: int 1 bnz wat` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(text, v) require.Error(t, err) @@ -717,7 +737,7 @@ byte b64 //GWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz+8= byte b64 avGWRM+yy3BCavBDXO/FYTNZ6o2Jai5edsMCBdDEz//= == ||` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(text, v) require.NoError(t, err) @@ -731,7 +751,7 @@ func TestAssembleRejectUnkLabel(t *testing.T) { t.Parallel() text := `int 1 bnz nowhere` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(text, v) require.Error(t, err) @@ -746,11 +766,11 @@ intc 0 intc 0 bnz done done:` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 9, len(program)) expectedProgBytes := []byte("\x01\x20\x01\x01\x22\x22\x40\x00\x00") - expectedProgBytes[0] = byte(AssemblerDefaultVersion) + expectedProgBytes[0] = byte(AssemblerMaxVersion) require.Equal(t, expectedProgBytes, program) } @@ -786,6 +806,7 @@ global GroupSize global LogicSigVersion global Round global LatestTimestamp +global CurrentApplicationID txn Sender txn Fee bnz label1 @@ -820,6 +841,22 @@ txn Accounts txn NumAccounts txn ApprovalProgram txn ClearStateProgram +txn RekeyTo +txn ConfigAsset +txn ConfigAssetTotal +txn ConfigAssetDecimals +txn ConfigAssetDefaultFrozen +txn ConfigAssetUnitName +txn ConfigAssetName +txn ConfigAssetURL +txn ConfigAssetMetadataHash +txn ConfigAssetManager +txn ConfigAssetReserve +txn ConfigAssetFreeze +txn ConfigAssetClawback +txn FreezeAsset +txn FreezeAssetAccount +txn FreezeAssetFrozen gtxn 12 Fee ` for _, globalField := range GlobalFieldNames { @@ -832,7 +869,7 @@ gtxn 12 Fee t.Errorf("TestAssembleDisassemble missing field txn %v", txnField) } } - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) t2, err := Disassemble(program) require.Equal(t, text, t2) @@ -867,7 +904,7 @@ func TestAssembleDisassembleCycle(t *testing.T) { func TestAssembleDisassembleErrors(t *testing.T) { source := `txn Sender` - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[2] = 0x50 // txn field _, err = Disassemble(program) @@ -875,7 +912,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid txn arg index") source = `txna Accounts 0` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[2] = 0x50 // txn field _, err = Disassemble(program) @@ -883,7 +920,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid txn arg index") source = `gtxn 0 Sender` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[3] = 0x50 // txn field _, err = Disassemble(program) @@ -891,7 +928,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid txn arg index") source = `gtxna 0 Accounts 0` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[3] = 0x50 // txn field _, err = Disassemble(program) @@ -899,7 +936,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid txn arg index") source = `global MinTxnFee` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[2] = 0x50 // txn field _, err = Disassemble(program) @@ -918,7 +955,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid opcode") source = "int 0\nint 0\nasset_holding_get AssetFrozen" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[7] = 0x50 // holding field _, err = Disassemble(program) @@ -926,7 +963,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid asset holding arg index") source = "int 0\nint 0\nasset_params_get AssetTotal" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[7] = 0x50 // params field _, err = Disassemble(program) @@ -934,7 +971,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "invalid asset params arg index") source = "int 0\nint 0\nasset_params_get AssetTotal" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Disassemble(program) require.NoError(t, err) @@ -944,7 +981,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "unexpected asset_params_get opcode end: missing 1 bytes") source = "gtxna 0 Accounts 0" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Disassemble(program) require.NoError(t, err) @@ -954,7 +991,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "unexpected gtxna opcode end: missing 2 bytes") source = "txna Accounts 0" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Disassemble(program) require.NoError(t, err) @@ -964,7 +1001,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Contains(t, err.Error(), "unexpected txna opcode end: missing 1 bytes") source = "byte 0x4141\nsubstring 0 1" - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Disassemble(program) require.NoError(t, err) @@ -978,7 +1015,7 @@ func TestAssembleVersions(t *testing.T) { text := `int 1 txna Accounts 0 ` - _, err := AssembleString(text) + _, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) _, err = AssembleStringV2(text) @@ -996,29 +1033,29 @@ func TestAssembleBalance(t *testing.T) { balance int 1 ==` - _, err := AssembleString(text) + _, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "balance arg 0 wanted type uint64 got []byte") } func TestAssembleAsset(t *testing.T) { source := "int 0\nint 0\nasset_holding_get ABC 1" - _, err := AssembleString(source) + _, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_holding_get expects one argument") source = "int 0\nint 0\nasset_holding_get ABC" - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_holding_get unknown arg") source = "int 0\nint 0\nasset_params_get ABC 1" - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_params_get expects one argument") source = "int 0\nint 0\nasset_params_get ABC" - _, err = AssembleString(source) + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_params_get unknown arg") } @@ -1026,7 +1063,7 @@ func TestAssembleAsset(t *testing.T) { func TestDisassembleSingleOp(t *testing.T) { // test ensures no double arg_0 entries in disassebly listing sample := "// version 2\narg_0\n" - program, err := AssembleString(sample) + program, err := AssembleStringWithVersion(sample, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 2, len(program)) disassembled, err := Disassemble(program) @@ -1036,7 +1073,7 @@ func TestDisassembleSingleOp(t *testing.T) { func TestAssembleOffsets(t *testing.T) { source := "err" - program, offsets, err := AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + program, offsets, err := AssembleStringWithVersionEx(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 2, len(program)) require.Equal(t, 1, len(offsets)) @@ -1053,7 +1090,7 @@ func TestAssembleOffsets(t *testing.T) { // comment err ` - program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 3, len(program)) require.Equal(t, 2, len(offsets)) @@ -1076,7 +1113,7 @@ err label1: err ` - program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 7, len(program)) require.Equal(t, 4, len(offsets)) @@ -1113,7 +1150,7 @@ err // comment ! ` - program, offsets, err = AssembleStringWithVersionEx(source, AssemblerDefaultVersion) + program, offsets, err = AssembleStringWithVersionEx(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, 6, len(program)) require.Equal(t, 2, len(offsets)) @@ -1133,7 +1170,7 @@ err func TestHasStatefulOps(t *testing.T) { source := "int 1" - program, err := AssembleStringWithVersion(source, AssemblerDefaultVersion) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) has, err := HasStatefulOps(program) require.NoError(t, err) @@ -1144,7 +1181,7 @@ int 1 app_opted_in err ` - program, err = AssembleStringWithVersion(source, AssemblerDefaultVersion) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) has, err = HasStatefulOps(program) require.NoError(t, err) @@ -1228,7 +1265,7 @@ func TestStringLiteralParsing(t *testing.T) { } func TestPragmaStream(t *testing.T) { - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { text := fmt.Sprintf("#pragma version %d", v) sr := strings.NewReader(text) ps := PragmaStream{} @@ -1340,50 +1377,53 @@ func TestAssemblePragmaVersion(t *testing.T) { text := `#pragma version 1 int 1 ` - program, _, err := AssembleStringWithVersionEx(text, 1) + program, err := AssembleStringWithVersion(text, 1) require.NoError(t, err) program1, err := AssembleStringV1("int 1") require.NoError(t, err) require.Equal(t, program1, program) - _, _, err = AssembleStringWithVersionEx(text, 0) + _, err = AssembleStringWithVersion(text, 0) require.Error(t, err) require.Contains(t, err.Error(), "version mismatch") - _, _, err = AssembleStringWithVersionEx(text, 2) + _, err = AssembleStringWithVersion(text, 2) require.Error(t, err) require.Contains(t, err.Error(), "version mismatch") - program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + program, err = AssembleStringWithVersion(text, assemblerNoVersion) require.NoError(t, err) require.Equal(t, program1, program) text = `#pragma version 2 int 1 ` - program, _, err = AssembleStringWithVersionEx(text, 2) + program, err = AssembleStringWithVersion(text, 2) require.NoError(t, err) program2, err := AssembleStringV2("int 1") require.NoError(t, err) require.Equal(t, program2, program) - _, _, err = AssembleStringWithVersionEx(text, 0) + _, err = AssembleStringWithVersion(text, 0) require.Error(t, err) require.Contains(t, err.Error(), "version mismatch") - _, _, err = AssembleStringWithVersionEx(text, 1) + _, err = AssembleStringWithVersion(text, 1) require.Error(t, err) require.Contains(t, err.Error(), "version mismatch") - program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + program, err = AssembleStringWithVersion(text, assemblerNoVersion) require.NoError(t, err) require.Equal(t, program2, program) - // check if no version it defaults to the latest one + // check if no version it defaults to TEAL v1 text = `byte "test" -substring 1 3 +len ` - program, _, err = AssembleStringWithVersionEx(text, assemblerNoVersion) + program, err = AssembleStringWithVersion(text, assemblerNoVersion) + require.NoError(t, err) + program1, err = AssembleStringV1(text) + require.Equal(t, program1, program) require.NoError(t, err) program2, err = AssembleString(text) require.NoError(t, err) @@ -1396,7 +1436,7 @@ substring 1 3 func TestAssembleConstants(t *testing.T) { t.Parallel() - for v := uint64(1); v <= LogicVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { _, err := AssembleStringWithVersion("intc 1", v) require.Error(t, err) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index 05c745499d..1c6fb3af07 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -261,8 +261,12 @@ func TestBackwardCompatTEALv1(t *testing.T) { program, err := hex.DecodeString(programTEALv1) require.NoError(t, err) - // ensure old program is the same as a new one except TEAL version byte - program2, err := AssembleString(sourceTEALv1) + // ensure old program is the same as a new one when assembling without version + program1, err := AssembleString(sourceTEALv1) + require.NoError(t, err) + require.Equal(t, program, program1) + // ensure the old program is the same as a new one except TEAL version byte + program2, err := AssembleStringWithVersion(sourceTEALv1, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, program[1:], program2[1:]) @@ -275,6 +279,7 @@ func TestBackwardCompatTEALv1(t *testing.T) { txgroup := makeSampleTxnGroup(txn) txn.Lsig.Logic = program txn.Lsig.Args = [][]byte{data[:], sig[:], pk[:], txn.Txn.Sender[:], txn.Txn.Note} + txn.Txn.RekeyTo = basics.Address{} // RekeyTo not allowed in TEAL v1 sb := strings.Builder{} ep := defaultEvalParams(&sb, &txn) @@ -339,15 +344,20 @@ func TestBackwardCompatGlobalFields(t *testing.T) { // check V1 assembler fails program, err := AssembleStringWithVersion(text, 0) require.Error(t, err) - require.Contains(t, err.Error(), "global unknown arg") + require.Contains(t, err.Error(), "available in version 2") require.Nil(t, program) program, err = AssembleStringWithVersion(text, 1) require.Error(t, err) - require.Contains(t, err.Error(), "global unknown arg") + require.Contains(t, err.Error(), "available in version 2") require.Nil(t, program) program, err = AssembleString(text) + require.Error(t, err) + require.Contains(t, err.Error(), "available in version 2") + require.Nil(t, program) + + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) proto := config.Consensus[protocol.ConsensusV23] @@ -407,15 +417,20 @@ func TestBackwardCompatTxnFields(t *testing.T) { // check V1 assembler fails program, err := AssembleStringWithVersion(text, 0) require.Error(t, err) - require.Contains(t, err.Error(), "txn unknown arg") + require.Contains(t, err.Error(), "available in version 2") require.Nil(t, program) program, err = AssembleStringWithVersion(text, 1) require.Error(t, err) - require.Contains(t, err.Error(), "txn unknown arg") + require.Contains(t, err.Error(), "available in version 2") require.Nil(t, program) program, err = AssembleString(text) + require.Error(t, err) + require.Contains(t, err.Error(), "available in version 2") + require.Nil(t, program) + + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) proto := config.Consensus[protocol.ConsensusV23] diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go index 9521c176bb..98b4a54469 100644 --- a/data/transactions/logic/debugger.go +++ b/data/transactions/logic/debugger.go @@ -185,6 +185,14 @@ func stackValueToTealValue(sv *stackValue) basics.TealValue { } } +func valueDeltaToValueDelta(vd *basics.ValueDelta) basics.ValueDelta { + return basics.ValueDelta{ + Action: vd.Action, + Bytes: base64.StdEncoding.EncodeToString([]byte(vd.Bytes)), + Uint: vd.Uint, + } +} + func (cx *evalContext) refreshDebugState() *DebugState { ds := &cx.debugState @@ -211,13 +219,13 @@ func (cx *evalContext) refreshDebugState() *DebugState { if (cx.runModeFlags & runModeApplication) != 0 { if cx.globalStateCow != nil { for k, v := range cx.globalStateCow.delta { - ds.GlobalStateChanges[k] = v + ds.GlobalStateChanges[k] = valueDeltaToValueDelta(&v) } } for addr, cow := range cx.localStateCows { delta := make(basics.StateDelta, len(cow.cow.delta)) for k, v := range cow.cow.delta { - delta[k] = v + delta[k] = valueDeltaToValueDelta(&v) } ds.LocalStateChanges[addr] = delta } diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index a5e49cbbd2..ded987a192 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -64,6 +64,7 @@ var opDocList = []stringString{ {"^", "A bitwise-xor B"}, {"~", "bitwise invert value X"}, {"mulw", "A times B out to 128-bit long result as low (top) and high uint64 values on the stack"}, + {"addw", "A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack"}, {"intcblock", "load block of uint64 constants"}, {"intc", "push value from uint64 constants to stack by index into constants"}, {"intc_0", "push constant 0 from intcblock to stack"}, @@ -98,12 +99,12 @@ var opDocList = []stringString{ {"concat", "pop two byte strings A and B and join them, push the result"}, {"substring", "pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result"}, {"substring3", "pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result"}, - {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction"}, + {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender"}, {"app_opted_in", "check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1}"}, {"app_local_get", "read from account specified by Txn.Accounts[A] from local state of the current application key B => value"}, {"app_local_get_ex", "read from account specified by Txn.Accounts[A] from local state of the application B key C => {0 or 1 (top), value}"}, {"app_global_get", "read key A from global state of a current application => value"}, - {"app_global_get_ex", "read from application A global state key B => {0 or 1 (top), value}"}, + {"app_global_get_ex", "read from application Txn.ForeignApps[A] global state key B => {0 or 1 (top), value}. A is specified as an account index in the ForeignApps field of the ApplicationCall transaction, zero index means this app"}, {"app_local_put", "write to account specified by Txn.Accounts[A] to local state of a current application key B with value C"}, {"app_global_put", "write key A and value B to global state of the current application"}, {"app_local_del", "delete from account specified by Txn.Accounts[A] local state key B of the current application"}, @@ -162,6 +163,7 @@ var opDocExtraList = []stringString{ {"intcblock", "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script."}, {"bytecblock", "`bytecblock` loads the following program bytes into an array of byte string constants in the evaluator. These constants can be referred to by `bytec` and `bytec_*` which will push the value onto the stack. Subsequent calls to `bytecblock` reset and replace the bytes constants available to the script."}, {"*", "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `mulw`."}, + {"+", "Overflow is an error condition which halts execution and fails the transaction. Full precision is available from `addw`."}, {"txn", "FirstValidTime causes the program to fail. The field is reserved for future use."}, {"gtxn", "for notes on transaction fields available, see `txn`. If this transaction is _i_ in the group, `gtxn i field` is equivalent to `txn field`."}, {"btoi", "`btoi` panics if the input is longer than 8 bytes."}, @@ -169,7 +171,7 @@ var opDocExtraList = []stringString{ {"app_opted_in", "params: account index, application id (top of the stack on opcode entry). Return: 1 if opted in and 0 otherwise."}, {"app_local_get", "params: account index, state key. Return: value. The value is zero if the key does not exist."}, {"app_local_get_ex", "params: account index, application id, state key. Return: did_exist flag (top of the stack, 1 if exist and 0 otherwise), value."}, - {"app_global_get_ex", "params: application id, state key. Return: value."}, + {"app_global_get_ex", "params: application index, state key. Return: value. Application index is"}, {"app_global_get", "params: state key. Return: value. The value is zero if the key does not exist."}, {"app_local_put", "params: account index, state key, value."}, {"app_local_del", "params: account index, state key."}, @@ -197,7 +199,7 @@ type OpGroup struct { // OpGroupList is groupings of ops for documentation purposes. var OpGroupList = []OpGroup{ - {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw", "concat", "substring", "substring3"}}, + {"Arithmetic", []string{"sha256", "keccak256", "sha512_256", "ed25519verify", "+", "-", "/", "*", "<", ">", "<=", ">=", "&&", "||", "==", "!=", "!", "len", "itob", "btoi", "%", "|", "&", "^", "~", "mulw", "addw", "concat", "substring", "substring3"}}, {"Loading Values", []string{"intcblock", "intc", "intc_0", "intc_1", "intc_2", "intc_3", "bytecblock", "bytec", "bytec_0", "bytec_1", "bytec_2", "bytec_3", "arg", "arg_0", "arg_1", "arg_2", "arg_3", "txn", "gtxn", "txna", "gtxna", "global", "load", "store"}}, {"Flow Control", []string{"err", "bnz", "bz", "b", "return", "pop", "dup", "dup2"}}, {"State Access", []string{"balance", "app_opted_in", "app_local_get", "app_local_get_ex", "app_global_get", "app_global_get_ex", "app_local_put", "app_global_put", "app_local_del", "app_global_del", "asset_holding_get", "asset_params_get"}}, @@ -304,6 +306,22 @@ var txnFieldDocList = []stringString{ {"NumAccounts", "Number of Accounts"}, {"ApprovalProgram", "Approval program"}, {"ClearStateProgram", "Clear state program"}, + {"RekeyTo", "32 byte Sender's new AuthAddr"}, + {"ConfigAsset", "Asset ID in asset config transaction"}, + {"ConfigAssetTotal", "Total number of units of this asset created"}, + {"ConfigAssetDecimals", "Number of digits to display after the decimal place when displaying the asset"}, + {"ConfigAssetDefaultFrozen", "Whether the asset's slots are frozen by default or not, 0 or 1"}, + {"ConfigAssetUnitName", "Unit name of the asset"}, + {"ConfigAssetName", "The asset name"}, + {"ConfigAssetURL", "URL"}, + {"ConfigAssetMetadataHash", "32 byte commitment to some unspecified asset metadata"}, + {"ConfigAssetManager", "32 byte address"}, + {"ConfigAssetReserve", "32 byte address"}, + {"ConfigAssetFreeze", "32 byte address"}, + {"ConfigAssetClawback", "32 byte address"}, + {"FreezeAsset", "Asset ID being frozen or un-frozen"}, + {"FreezeAssetAccount", "32 byte address of the account whose asset slot is being frozen or un-frozen"}, + {"FreezeAssetFrozen", "The new frozen value, 0 or 1"}, } // TxnFieldDocs are notes on fields available by `txn` and `gtxn` @@ -323,6 +341,7 @@ var globalFieldDocList = []stringString{ {"LogicSigVersion", "Maximum supported TEAL version"}, {"Round", "Current round number"}, {"LatestTimestamp", "Last confirmed block UNIX timestamp. Fails if negative"}, + {"CurrentApplicationID", "ID of current application executing. Fails if no such application is executing"}, } // globalFieldDocs are notes on fields available in `global` @@ -369,7 +388,7 @@ var assetParamsFieldDocList = []stringString{ {"AssetDecimals", "See AssetParams.Decimals"}, {"AssetDefaultFrozen", "Frozen by default or not"}, {"AssetUnitName", "Asset unit name"}, - {"AssetAssetName", "Asset name"}, + {"AssetName", "Asset name"}, {"AssetURL", "URL with additional info about the asset"}, {"AssetMetadataHash", "Arbitrary commitment"}, {"AssetManager", "Manager commitment"}, diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index 6031df10c1..2629be942a 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -80,7 +80,7 @@ func TestOpImmediateNote(t *testing.T) { func TestOpDocExtra(t *testing.T) { xd := OpDocExtra("bnz") require.NotEmpty(t, xd) - xd = OpDocExtra("+") + xd = OpDocExtra("-") require.Empty(t, xd) } diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 5e3d080025..b5341e0f0a 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -108,6 +108,8 @@ type LedgerForLogic interface { AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) AssetParams(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetParams, error) ApplicationID() basics.AppIndex + LocalSchema() basics.StateSchema + GlobalSchema() basics.StateSchema } // EvalParams contains data that comes into condition evaluation. @@ -369,13 +371,6 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { return false, cx.err } - // Currently no TEAL version knows about the RekeyTo field, but this - // may change in a future TEAL version - if !cx.EvalParams.Txn.Txn.RekeyTo.IsZero() { - cx.err = fmt.Errorf("program version %d doesn't allow transactions with nonzero RekeyTo field", version) - return false, cx.err - } - // TODO: if EvalMaxVersion > version, ensure that inaccessible // fields as of the program's version are zero or other // default value so that no one is hiding unexpected @@ -386,6 +381,11 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { cx.stack = make([]stackValue, 0, 10) cx.program = program + err = cx.checkRekeyAllowed() + if err != nil { + return + } + if cx.Debugger != nil { cx.debugState = makeDebugState(cx) if err = cx.Debugger.Register(cx.refreshDebugState()); err != nil { @@ -476,10 +476,17 @@ func check(program []byte, params EvalParams) (cost int, err error) { err = fmt.Errorf("program version %d greater than protocol supported version %d", version, params.Proto.LogicSigVersion) return } + cx.version = version cx.pc = vlen cx.EvalParams = params cx.program = program + + err = cx.checkRekeyAllowed() + if err != nil { + return + } + for (cx.err == nil) && (cx.pc < len(cx.program)) { prevpc := cx.pc cost += cx.checkStep() @@ -495,6 +502,16 @@ func check(program []byte, params EvalParams) (cost int, err error) { return } +func (cx *evalContext) checkRekeyAllowed() error { + // A transaction may not have a nonzero RekeyTo field set before TEAL + // v2, otherwise TEAL v0 or v1 accounts could be rekeyed by an anyone + // who could ordinarily spend from them. + if cx.version < rekeyingEnabledVersion && !cx.EvalParams.Txn.Txn.RekeyTo.IsZero() { + return fmt.Errorf("program version %d doesn't allow transactions with nonzero RekeyTo field", cx.version) + } + return nil +} + func opCompat(expected, got StackType) bool { if expected == StackAny { return true @@ -509,6 +526,13 @@ func nilToEmpty(x []byte) []byte { return x } +func boolToUint(x bool) uint64 { + if x { + return 1 + } + return 0 +} + // MaxStackDepth should move to consensus params const MaxStackDepth = 1000 @@ -689,6 +713,22 @@ func opPlus(cx *evalContext) { cx.stack = cx.stack[:last] } +func opAddwImpl(x, y uint64) (carry uint64, sum uint64) { + sum = x + y + if sum < x { + carry = 1 + } + return +} + +func opAddw(cx *evalContext) { + last := len(cx.stack) - 1 + prev := last - 1 + carry, sum := opAddwImpl(cx.stack[prev].Uint, cx.stack[last].Uint) + cx.stack[prev].Uint = carry + cx.stack[last].Uint = sum +} + func opMinus(cx *evalContext) { last := len(cx.stack) - 1 prev := last - 1 @@ -1125,11 +1165,7 @@ func (cx *evalContext) assetHoldingEnumToValue(holding *basics.AssetHolding, fie case AssetBalance: sv.Uint = holding.Amount case AssetFrozen: - if holding.Frozen { - sv.Uint = 1 - } else { - sv.Uint = 0 - } + sv.Uint = boolToUint(holding.Frozen) default: err = fmt.Errorf("invalid asset holding field %d", field) return @@ -1150,14 +1186,10 @@ func (cx *evalContext) assetParamsEnumToValue(params *basics.AssetParams, field case AssetDecimals: sv.Uint = uint64(params.Decimals) case AssetDefaultFrozen: - if params.DefaultFrozen { - sv.Uint = 1 - } else { - sv.Uint = 0 - } + sv.Uint = boolToUint(params.DefaultFrozen) case AssetUnitName: sv.Bytes = []byte(params.UnitName) - case AssetAssetName: + case AssetName: sv.Bytes = []byte(params.AssetName) case AssetURL: sv.Bytes = []byte(params.URL) @@ -1266,8 +1298,7 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF err = fmt.Errorf("invalid ApplicationArgs index %d", arrayFieldIdx) return } - sv.Bytes = make([]byte, len(txn.ApplicationArgs[arrayFieldIdx])) - copy(sv.Bytes, txn.ApplicationArgs[arrayFieldIdx]) + sv.Bytes = nilToEmpty(txn.ApplicationArgs[arrayFieldIdx]) case NumAppArgs: sv.Uint = uint64(len(txn.ApplicationArgs)) case Accounts: @@ -1284,11 +1315,41 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF case NumAccounts: sv.Uint = uint64(len(txn.Accounts)) case ApprovalProgram: - sv.Bytes = make([]byte, len(txn.ApprovalProgram)) - copy(sv.Bytes, txn.ApprovalProgram) + sv.Bytes = nilToEmpty(txn.ApprovalProgram) case ClearStateProgram: - sv.Bytes = make([]byte, len(txn.ClearStateProgram)) - copy(sv.Bytes, txn.ClearStateProgram) + sv.Bytes = nilToEmpty(txn.ClearStateProgram) + case RekeyTo: + sv.Bytes = txn.RekeyTo[:] + case ConfigAsset: + sv.Uint = uint64(txn.ConfigAsset) + case ConfigAssetTotal: + sv.Uint = uint64(txn.AssetParams.Total) + case ConfigAssetDecimals: + sv.Uint = uint64(txn.AssetParams.Decimals) + case ConfigAssetDefaultFrozen: + sv.Uint = boolToUint(txn.AssetParams.DefaultFrozen) + case ConfigAssetUnitName: + sv.Bytes = nilToEmpty([]byte(txn.AssetParams.UnitName)) + case ConfigAssetName: + sv.Bytes = nilToEmpty([]byte(txn.AssetParams.AssetName)) + case ConfigAssetURL: + sv.Bytes = nilToEmpty([]byte(txn.AssetParams.URL)) + case ConfigAssetMetadataHash: + sv.Bytes = nilToEmpty(txn.AssetParams.MetadataHash[:]) + case ConfigAssetManager: + sv.Bytes = txn.AssetParams.Manager[:] + case ConfigAssetReserve: + sv.Bytes = txn.AssetParams.Reserve[:] + case ConfigAssetFreeze: + sv.Bytes = txn.AssetParams.Freeze[:] + case ConfigAssetClawback: + sv.Bytes = txn.AssetParams.Clawback[:] + case FreezeAsset: + sv.Uint = uint64(txn.FreezeAsset) + case FreezeAssetAccount: + sv.Bytes = txn.FreezeAccount[:] + case FreezeAssetFrozen: + sv.Uint = boolToUint(txn.AssetFrozen) default: err = fmt.Errorf("invalid txn field %d", field) return @@ -1305,7 +1366,12 @@ func (cx *evalContext) txnFieldToStack(txn *transactions.Transaction, field TxnF func opTxn(cx *evalContext) { field := TxnField(uint64(cx.program[cx.pc+1])) fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version || field == ApplicationArgs || field == Accounts { + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + _, ok = txnaFieldSpecByField[field] + if ok { cx.err = fmt.Errorf("invalid txn field %d", field) return } @@ -1327,7 +1393,8 @@ func opTxna(cx *evalContext) { cx.err = fmt.Errorf("invalid txn field %d", field) return } - if field != ApplicationArgs && field != Accounts { + _, ok = txnaFieldSpecByField[field] + if !ok { cx.err = fmt.Errorf("txna unsupported field %d", field) return } @@ -1352,7 +1419,12 @@ func opGtxn(cx *evalContext) { tx := &cx.TxnGroup[gtxid].Txn field := TxnField(uint64(cx.program[cx.pc+2])) fs, ok := txnFieldSpecByField[field] - if !ok || fs.version > cx.version || field == ApplicationArgs || field == Accounts { + if !ok || fs.version > cx.version { + cx.err = fmt.Errorf("invalid txn field %d", field) + return + } + _, ok = txnaFieldSpecByField[field] + if ok { cx.err = fmt.Errorf("invalid txn field %d", field) return } @@ -1385,7 +1457,8 @@ func opGtxna(cx *evalContext) { cx.err = fmt.Errorf("invalid txn field %d", field) return } - if TxnField(field) != ApplicationArgs && TxnField(field) != Accounts { + _, ok = txnaFieldSpecByField[field] + if !ok { cx.err = fmt.Errorf("gtxna unsupported field %d", field) return } @@ -1422,6 +1495,14 @@ func (cx *evalContext) getLatestTimestamp() (timestamp uint64, err error) { return uint64(ts), nil } +func (cx *evalContext) getApplicationID() (rnd uint64, err error) { + if cx.Ledger == nil { + err = fmt.Errorf("ledger not available") + return + } + return uint64(cx.Ledger.ApplicationID()), nil +} + var zeroAddress basics.Address func (cx *evalContext) globalFieldToStack(field GlobalField) (sv stackValue, err error) { @@ -1442,6 +1523,8 @@ func (cx *evalContext) globalFieldToStack(field GlobalField) (sv stackValue, err sv.Uint, err = cx.getRound() case LatestTimestamp: sv.Uint, err = cx.getLatestTimestamp() + case CurrentApplicationID: + sv.Uint, err = cx.getApplicationID() default: err = fmt.Errorf("invalid global[%d]", field) } @@ -1686,7 +1769,10 @@ func (cx *evalContext) getLocalStateCow(accountIdx uint64) (*keyValueCow, error) } localDelta := make(basics.StateDelta) - kvCow := makeKeyValueCow(localKV, localDelta) + kvCow, err := makeKeyValueCow(localKV, localDelta, cx.Ledger.LocalSchema(), cx.Proto) + if err != nil { + return nil, err + } idxCow = &indexedCow{accountIdx, kvCow} cx.localStateCows[addr] = idxCow } @@ -1721,8 +1807,7 @@ func (cx *evalContext) appWriteLocalKey(accountIdx uint64, key string, tv basics if err != nil { return err } - kvCow.write(key, tv) - return nil + return kvCow.write(key, tv) } // appDeleteLocalKey deletes a value from the key/value cow @@ -1731,8 +1816,7 @@ func (cx *evalContext) appDeleteLocalKey(accountIdx uint64, key string) error { if err != nil { return err } - kvCow.del(key) - return nil + return kvCow.del(key) } func (cx *evalContext) getReadOnlyGlobalState(appID uint64) (basics.TealKeyValue, error) { @@ -1750,20 +1834,38 @@ func (cx *evalContext) getReadOnlyGlobalState(appID uint64) (basics.TealKeyValue func (cx *evalContext) getGlobalStateCow() (*keyValueCow, error) { if cx.globalStateCow == nil { - globalKV, err := cx.Ledger.AppGlobalState(cx.Txn.Txn.ApplicationID) + appIdx := cx.Ledger.ApplicationID() + globalKV, err := cx.Ledger.AppGlobalState(appIdx) if err != nil { return nil, fmt.Errorf("failed to fetch global state: %v", err) } - cx.globalStateCow = makeKeyValueCow(globalKV, cx.appEvalDelta.GlobalDelta) + cx.globalStateCow, err = makeKeyValueCow(globalKV, cx.appEvalDelta.GlobalDelta, cx.Ledger.GlobalSchema(), cx.Proto) + if err != nil { + return nil, err + } } return cx.globalStateCow, nil } -func (cx *evalContext) appReadGlobalKey(appID uint64, key string) (basics.TealValue, bool, error) { - // If this is for the application mentioned in the transaction header, +func (cx *evalContext) appReadGlobalKey(foreignAppsIndex uint64, key string) (basics.TealValue, bool, error) { + // If this is for the current app (ForeignApps index zero), // return the result from a GlobalState cow, since we may have written // to it - if appID == 0 || appID == uint64(cx.Ledger.ApplicationID()) { + if foreignAppsIndex > uint64(len(cx.Txn.Txn.ForeignApps)) { + err := fmt.Errorf("invalid ForeignApps index %d", foreignAppsIndex) + return basics.TealValue{}, false, err + } + + var appIdx basics.AppIndex + if foreignAppsIndex == 0 { + appIdx = 0 + } else { + appIdx = cx.Txn.Txn.ForeignApps[foreignAppsIndex-1] + if appIdx == cx.Ledger.ApplicationID() { + appIdx = 0 + } + } + if appIdx == 0 { kvCow, err := cx.getGlobalStateCow() if err != nil { return basics.TealValue{}, false, err @@ -1773,7 +1875,7 @@ func (cx *evalContext) appReadGlobalKey(appID uint64, key string) (basics.TealVa } // Otherwise, the state is read only, so return from the read only cache - kv, err := cx.getReadOnlyGlobalState(appID) + kv, err := cx.getReadOnlyGlobalState(uint64(appIdx)) if err != nil { return basics.TealValue{}, false, err } @@ -1787,8 +1889,7 @@ func (cx *evalContext) appWriteGlobalKey(key string, tv basics.TealValue) error if err != nil { return err } - kvCow.write(key, tv) - return nil + return kvCow.write(key, tv) } // appDeleteGlobalKey deletes a value from the cache and adds it to StateDelta @@ -1797,8 +1898,7 @@ func (cx *evalContext) appDeleteGlobalKey(key string) error { if err != nil { return err } - kvCow.del(key) - return nil + return kvCow.del(key) } func opAppGetLocalState(cx *evalContext) { @@ -1862,13 +1962,13 @@ func opAppGetLocalStateImpl(cx *evalContext, appID uint64, key []byte, accountId return } -func opAppGetGlobalStateImpl(cx *evalContext, appID uint64, key []byte) (result stackValue, ok bool, err error) { +func opAppGetGlobalStateImpl(cx *evalContext, appIndex uint64, key []byte) (result stackValue, ok bool, err error) { if cx.Ledger == nil { err = fmt.Errorf("ledger not available") return } - tv, ok, err := cx.appReadGlobalKey(appID, string(key)) + tv, ok, err := cx.appReadGlobalKey(appIndex, string(key)) if err != nil { return } @@ -1884,8 +1984,8 @@ func opAppGetGlobalState(cx *evalContext) { key := cx.stack[last].Bytes - var appID uint64 = 0 - result, _, err := opAppGetGlobalStateImpl(cx, appID, key) + var index uint64 = 0 // index in txn.ForeignApps + result, _, err := opAppGetGlobalStateImpl(cx, index, key) if err != nil { cx.err = err return @@ -1899,13 +1999,9 @@ func opAppGetGlobalStateEx(cx *evalContext) { prev := last - 1 key := cx.stack[last].Bytes - appID := cx.stack[prev].Uint - - if appID != 0 && appID == uint64(cx.Txn.Txn.ApplicationID) { - appID = 0 // 0 is an alias for the current app - } + index := cx.stack[prev].Uint // index in txn.ForeignApps - result, ok, err := opAppGetGlobalStateImpl(cx, appID, key) + result, ok, err := opAppGetGlobalStateImpl(cx, index, key) if err != nil { cx.err = err return diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index e12c71833d..207af52e55 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -171,6 +171,20 @@ func (l *testLedger) ApplicationID() basics.AppIndex { return basics.AppIndex(l.appID) } +func (l *testLedger) LocalSchema() basics.StateSchema { + return basics.StateSchema{ + NumUint: 100, + NumByteSlice: 100, + } +} + +func (l *testLedger) GlobalSchema() basics.StateSchema { + return basics.StateSchema{ + NumUint: 100, + NumByteSlice: 100, + } +} + func TestEvalModes(t *testing.T) { t.Parallel() // ed25519verify and err are tested separately below @@ -347,7 +361,7 @@ pop for mode, test := range tests { t.Run(fmt.Sprintf("opcodes_mode=%d", mode), func(t *testing.T) { - program, err := AssembleString(test.source) + program, err := AssembleStringWithVersion(test.source, AssemblerMaxVersion) require.NoError(t, err) sb := strings.Builder{} ep := defaultEvalParams(&sb, &txn) @@ -369,7 +383,7 @@ pop for mode, test := range tests { t.Run(fmt.Sprintf("err_mode=%d", mode), func(t *testing.T) { source := "err" - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) _, err = test.check(program, ep) @@ -391,7 +405,7 @@ pop "arg_3", } for _, source := range disallowed { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) _, err = CheckStateful(program, ep) @@ -417,7 +431,7 @@ pop } for _, source := range newOpcodeCalls { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) _, err = Check(program, ep) @@ -440,7 +454,7 @@ func TestBalance(t *testing.T) { balance int 1 ==` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -465,7 +479,7 @@ int 1 balance int 1 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -478,7 +492,7 @@ int 1 balance int 1 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) var addr basics.Address copy(addr[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui02")) @@ -513,7 +527,7 @@ int 100 // app idx app_opted_in int 1 ==` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -540,7 +554,7 @@ int 100 // app idx app_opted_in int 0 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -555,7 +569,7 @@ int 100 // app idx app_opted_in int 0 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger := makeTestLedger( map[basics.Address]uint64{ @@ -578,7 +592,7 @@ int 1 ==` ledger.newApp(txn.Txn.Receiver, 100) - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, _, err = EvalStateful(program, ep) require.NoError(t, err) @@ -592,7 +606,7 @@ int 1 ==` ledger.newApp(txn.Txn.Sender, 100) - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, _, err = EvalStateful(program, ep) require.NoError(t, err) @@ -616,7 +630,7 @@ err exit: int 1 ==` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -654,7 +668,7 @@ exist: err exit: int 1` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) _, _, err = EvalStateful(program, ep) require.Error(t, err) @@ -690,7 +704,7 @@ byte 0x414c474f ==` ledger.balances[txn.Txn.Receiver].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) @@ -713,7 +727,7 @@ exist: byte 0x414c474f ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} @@ -734,7 +748,7 @@ exist: byte 0x414c474f ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger.balances[txn.Txn.Sender].apps[101][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} @@ -749,7 +763,7 @@ app_local_get byte 0x414c474f ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} @@ -764,7 +778,7 @@ app_local_get int 0 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} @@ -784,7 +798,7 @@ err exist: byte 0x414c474f == -int 100 +int 1 // ForeignApps index txn ApplicationArgs 0 app_global_get_ex bnz exist1 @@ -799,7 +813,7 @@ byte 0x414c474f == && ` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -822,6 +836,7 @@ byte 0x414c474f ep.Ledger = ledger ep.Txn.Txn.ApplicationID = 100 + ep.Txn.Txn.ForeignApps = []basics.AppIndex{ep.Txn.Txn.ApplicationID} _, _, err = EvalStateful(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "failed to fetch global state") @@ -834,6 +849,7 @@ byte 0x414c474f require.Contains(t, err.Error(), "err opcode") ledger.applications[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} + cost, err = CheckStateful(program, ep) require.NoError(t, err) require.True(t, cost < 1000) @@ -841,13 +857,25 @@ byte 0x414c474f require.NoError(t, err) require.True(t, pass) + // check error on invalid app index for app_global_get_ex + text = `int 2 +txn ApplicationArgs 0 +app_global_get_ex +` + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) + require.NoError(t, err) + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid ForeignApps index 2") + // check app_local_get default value text = `byte 0x414c474f55 app_global_get int 0 ==` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ledger.balances[txn.Txn.Sender].apps[100][string(protocol.PaymentTx)] = basics.TealValue{Type: basics.TealBytesType, Bytes: "ALGO"} @@ -859,20 +887,23 @@ int 0 byte 0x41414141 int 4141 app_global_put -int 100 +int 1 // ForeignApps index byte 0x41414141 app_global_get_ex +bnz exist +err +exist: int 4141 == -pop ` // check that even during application creation (Txn.ApplicationID == 0) // we will use the the kvCow if the exact application ID (100) is // specified in the transaction - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ep.Txn.Txn.ApplicationID = 0 + ep.Txn.Txn.ForeignApps = []basics.AppIndex{100} pass, _, err = EvalStateful(program, ep) require.NoError(t, err) require.True(t, pass) @@ -927,7 +958,7 @@ byte 0x414c474f && int 1 int 55 -asset_params_get AssetAssetName +asset_params_get AssetName ! bnz error len @@ -1009,7 +1040,7 @@ func TestAssets(t *testing.T) { } for _, source := range sources { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -1034,7 +1065,7 @@ func TestAssets(t *testing.T) { require.Contains(t, err.Error(), "cannot load account[5]") } - program, err := AssembleString(assetsTestProgram) + program, err := AssembleStringWithVersion(assetsTestProgram, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, defaultEvalParams(nil, nil)) require.NoError(t, err) @@ -1089,7 +1120,7 @@ err ok: int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: false}) cost, err = CheckStateful(program, ep) @@ -1120,7 +1151,7 @@ err ok: int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.DefaultFrozen = true ledger.setAsset(txn.Txn.Receiver, 55, params) @@ -1150,7 +1181,7 @@ err ok: int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" ledger.setAsset(txn.Txn.Receiver, 55, params) @@ -1171,7 +1202,7 @@ err ok: int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" ledger.setAsset(txn.Txn.Receiver, 55, params) @@ -1236,7 +1267,7 @@ int 100 source := test.source firstCmdOffset := test.accNumOffset - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -1335,7 +1366,7 @@ int 0x77 == && ` - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -1379,7 +1410,7 @@ int 0x77 algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -1416,7 +1447,7 @@ exist2: ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1436,7 +1467,7 @@ int 1 ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1468,7 +1499,7 @@ int 0x78 ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1498,7 +1529,7 @@ app_local_put ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue delete(ledger.balances[txn.Txn.Sender].apps[100], "ALGOA") - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1537,7 +1568,7 @@ int 1 ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 500) ledger.balances[txn.Txn.Receiver].apps[100] = make(map[string]basics.TealValue) - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -1599,7 +1630,7 @@ int 1 } for name, source := range tests { t.Run(fmt.Sprintf("test=%s", name), func(t *testing.T) { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -1667,7 +1698,7 @@ byte 0x414c474f == && // check generic with exact app id -int 100 // current app id +int 1 // ForeignApps index - current app byte 0x414c474f41 // key "ALGOA" app_global_get_ex bnz ok1 @@ -1683,7 +1714,7 @@ int 0x77 == && // check generic with alias -int 0 // current app id +int 0 // ForeignApps index - current app byte 0x414c474f app_global_get_ex bnz ok2 @@ -1693,7 +1724,7 @@ int 0x77 == && // check generic with exact app id -int 100 // current app id +int 1 // ForeignApps index - current app byte 0x414c474f app_global_get_ex bnz ok3 @@ -1706,6 +1737,7 @@ int 0x77 ep := defaultEvalParams(nil, nil) txn := makeSampleTxn() txn.Txn.ApplicationID = 100 + txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID} ep.Txn = &txn ledger := makeTestLedger( map[basics.Address]uint64{ @@ -1715,7 +1747,7 @@ int 0x77 ep.Ledger = ledger ledger.newApp(txn.Txn.Sender, 100) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -1753,7 +1785,7 @@ int 0x77 algoValue := basics.TealValue{Type: basics.TealUintType, Uint: 0x77} ledger.applications[100]["ALGO"] = algoValue - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1782,7 +1814,7 @@ int 0x77 delete(ledger.applications[100], "ALGOA") ledger.applications[100]["ALGO"] = algoValue - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -1827,7 +1859,7 @@ byte 0x414c474f delete(ledger.applications[100], "ALGOA") ledger.applications[100]["ALGO"] = algoValue - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) sb := strings.Builder{} ep.Trace = &sb @@ -1859,14 +1891,14 @@ byte 0x414c474f func TestAppGlobalReadOtherApp(t *testing.T) { t.Parallel() - source := `int 101 + source := `int 2 // ForeignApps index byte "mykey1" app_global_get_ex bz ok1 err ok1: pop -int 101 +int 2 // ForeignApps index byte "mykey" app_global_get_ex bnz ok2 @@ -1878,6 +1910,7 @@ byte "myval" ep := defaultEvalParams(nil, nil) txn := makeSampleTxn() txn.Txn.ApplicationID = 100 + txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID, 101} ep.Txn = &txn ledger := makeTestLedger( map[basics.Address]uint64{ @@ -1887,7 +1920,7 @@ byte "myval" ep.Ledger = ledger ledger.newApp(txn.Txn.Sender, 100) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -1954,7 +1987,7 @@ int 1 sb := strings.Builder{} ep.Trace = &sb - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -1981,13 +2014,13 @@ int 1 // check delete existing source = `byte 0x414c474f // key "ALGO" app_global_del -int 100 +int 1 byte 0x414c474f app_global_get_ex == // two zeros ` - - program, err = AssembleString(source) + ep.Txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID} + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -2018,7 +2051,7 @@ byte 0x414c474f41 int 0x78 app_global_put ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -2046,7 +2079,7 @@ int 0x78 app_global_put int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) pass, delta, err = EvalStateful(program, ep) require.NoError(t, err) @@ -2074,7 +2107,7 @@ byte 0x414c474f app_global_del int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2105,7 +2138,7 @@ byte 0x414c474f41 app_global_del int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2171,7 +2204,7 @@ int 1 sb := strings.Builder{} ep.Trace = &sb - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err := CheckStateful(program, ep) require.NoError(t, err) @@ -2208,7 +2241,7 @@ app_local_get_ex == // two zeros ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2245,7 +2278,7 @@ byte 0x414c474f41 int 0x78 app_local_put ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2278,7 +2311,7 @@ int 0x78 app_local_put int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2314,7 +2347,7 @@ byte 0x414c474f app_local_del int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2350,7 +2383,7 @@ byte 0x414c474f41 app_local_del int 1 ` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) cost, err = CheckStateful(program, ep) require.NoError(t, err) @@ -2374,7 +2407,7 @@ func TestEnumFieldErrors(t *testing.T) { TxnFieldTypes[Amount] = origTxnType }() - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Eval(program, ep) require.Error(t, err) @@ -2390,7 +2423,7 @@ func TestEnumFieldErrors(t *testing.T) { GlobalFieldTypes[MinTxnFee] = origGlobalType }() - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Eval(program, ep) require.Error(t, err) @@ -2434,7 +2467,7 @@ pop AssetHoldingFieldTypes[AssetBalance] = origAssetHoldingType }() - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, _, err = EvalStateful(program, ep) require.Error(t, err) @@ -2451,7 +2484,7 @@ pop AssetParamsFieldTypes[AssetTotal] = origAssetTotalType }() - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, _, err = EvalStateful(program, ep) require.Error(t, err) @@ -2472,6 +2505,7 @@ func TestReturnTypes(t *testing.T) { ep.Txn = &txn ep.TxnGroup = txgroup ep.Txn.Txn.ApplicationID = 1 + ep.Txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID} txn.Lsig.Args = [][]byte{ []byte("aoeu"), []byte("aoeu"), @@ -2550,7 +2584,7 @@ func TestReturnTypes(t *testing.T) { sb.WriteString(name + "\n") } source := sb.String() - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) var cx evalContext @@ -2596,7 +2630,7 @@ int 1 ledger := makeTestLedger( map[basics.Address]uint64{}, ) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) @@ -2625,7 +2659,37 @@ int 1 ledger := makeTestLedger( map[basics.Address]uint64{}, ) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) + require.NoError(t, err) + + ep := defaultEvalParams(nil, nil) + _, err = CheckStateful(program, ep) + require.NoError(t, err) + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "ledger not available") + + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "not allowed in current mode") + require.False(t, pass) + + ep.Ledger = ledger + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) +} + +func TestCurrentApplicationID(t *testing.T) { + source := `global CurrentApplicationID +int 42 +== +` + ledger := makeTestLedger( + map[basics.Address]uint64{}, + ) + ledger.appID = 42 + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 475649ce27..6e362c83d5 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -48,7 +48,12 @@ func defaultEvalProtoV1() config.ConsensusParams { } func defaultEvalProtoWithVersion(version uint64) config.ConsensusParams { - return config.ConsensusParams{LogicSigVersion: version, LogicSigMaxCost: 20000} + return config.ConsensusParams{ + LogicSigVersion: version, + LogicSigMaxCost: 20000, + MaxAppKeyLen: 64, + MaxAppBytesValueLen: 64, + } } func defaultEvalParamsV1(sb *strings.Builder, txn *transactions.SignedTxn) EvalParams { @@ -81,7 +86,7 @@ func defaultEvalParamsWithVersion(sb *strings.Builder, txn *transactions.SignedT func TestTooManyArgs(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) require.NoError(t, err) @@ -107,7 +112,7 @@ func TestEmptyProgram(t *testing.T) { func TestWrongProtoVersion(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) require.NoError(t, err) @@ -131,7 +136,7 @@ func TestWrongProtoVersion(t *testing.T) { func TestTrivialMath(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 2 int 3 @@ -153,7 +158,7 @@ int 5 func TestSha256EqArg(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`arg 0 sha256 @@ -211,7 +216,7 @@ int 1000000 func TestTLHC(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { a1, _ := basics.UnmarshalChecksumAddress("DFPKC2SJP3OTFVJFMCD356YB7BOT4SJZTGWLIPPFEWL3ZABUFLTOY6ILYE") @@ -298,7 +303,7 @@ func TestTLHC(t *testing.T) { func TestU64Math(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x1234567812345678 int 0x100000000 @@ -320,7 +325,7 @@ int 0x12345678 func TestItob(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`byte 0x1234567812345678 int 0x1234567812345678 @@ -341,7 +346,7 @@ itob func TestBtoi(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x1234567812345678 byte 0x1234567812345678 @@ -362,7 +367,7 @@ btoi func TestBtoiTooLong(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x1234567812345678 byte 0x1234567812345678aaaa @@ -384,7 +389,7 @@ btoi func TestBnz(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 dup @@ -411,7 +416,7 @@ int 1 func TestBnz2(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 int 2 @@ -446,7 +451,7 @@ pop func TestBz(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0 dup @@ -473,7 +478,7 @@ int 1 func TestB(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`b safe err @@ -497,7 +502,7 @@ int 1`, v) func TestReturn(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 return @@ -520,7 +525,7 @@ err`, v) func TestReturnFalse(t *testing.T) { t.Parallel() - for v := uint64(2); v <= AssemblerDefaultVersion; v++ { + for v := uint64(2); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0 return @@ -543,7 +548,7 @@ int 1`, v) func TestSubUnderflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 int 0x100000000 @@ -569,7 +574,7 @@ int 1`, v) func TestAddOverflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0xf000000000000000 int 0x1111111111111111 @@ -595,7 +600,7 @@ int 1`, v) func TestMulOverflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x111111111 int 0x222222222 @@ -645,7 +650,7 @@ func TestMulwImpl(t *testing.T) { func TestMulw(t *testing.T) { t.Parallel() // multiply two numbers, ensure high is 2 and low is 0x468acf130eca8642 - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x111111111 int 0x222222222 @@ -678,9 +683,64 @@ int 1 // ret 1 } } +func TestAddwImpl(t *testing.T) { + t.Parallel() + carry, sum := opAddwImpl(1, 2) + require.Equal(t, uint64(0), carry) + require.Equal(t, uint64(3), sum) + + carry, sum = opAddwImpl(0xFFFFFFFFFFFFFFFD, 0x45) + require.Equal(t, uint64(1), carry) + require.Equal(t, uint64(0x42), sum) + + carry, sum = opAddwImpl(0, 0) + require.Equal(t, uint64(0), carry) + require.Equal(t, uint64(0), sum) + + carry, sum = opAddwImpl((1<<64)-1, (1<<64)-1) + require.Equal(t, uint64(1), carry) + require.Equal(t, uint64((1<<64)-2), sum) +} + +func TestAddw(t *testing.T) { + t.Parallel() + // add two numbers, ensure sum is 0x42 and carry is 0x1 + for v := uint64(2); v <= AssemblerMaxVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 0xFFFFFFFFFFFFFFFF +int 0x43 +addw +int 0x42 // compare sum (top of the stack) +== +bnz continue +err +continue: +int 1 // compare carry +== +bnz done +err +done: +int 1 // ret 1 +`, v) + require.NoError(t, err) + cost, err := Check(program, defaultEvalParams(nil, nil)) + require.NoError(t, err) + require.True(t, cost < 1000) + sb := strings.Builder{} + pass, err := Eval(program, defaultEvalParams(&sb, nil)) + if !pass { + t.Log(hex.EncodeToString(program)) + t.Log(sb.String()) + } + require.True(t, pass) + require.NoError(t, err) + }) + } +} + func TestDivZero(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x111111111 int 0 @@ -710,7 +770,7 @@ int 1`, v) func TestModZero(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x111111111 int 0 @@ -736,7 +796,7 @@ int 1`, v) func TestErr(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`err int 1`, v) @@ -759,7 +819,7 @@ int 1`, v) func TestModSubMulOk(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 35 int 16 @@ -788,7 +848,7 @@ int 4 func TestPop(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 int 0 @@ -812,7 +872,7 @@ pop`, v) func TestStackLeftover(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 int 1`, v) @@ -836,7 +896,7 @@ int 1`, v) func TestStackBytesLeftover(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`byte 0x10101010`, v) require.NoError(t, err) @@ -859,7 +919,7 @@ func TestStackBytesLeftover(t *testing.T) { func TestStackEmpty(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 int 1 @@ -884,7 +944,7 @@ pop`, v) func TestArgTooFar(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`arg_1 btoi`, v) @@ -910,7 +970,7 @@ btoi`, v) func TestIntcTooFar(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`intc_1`, v) require.NoError(t, err) @@ -935,7 +995,7 @@ func TestIntcTooFar(t *testing.T) { func TestBytecTooFar(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`bytec_1 btoi`, v) @@ -985,7 +1045,7 @@ func TestTxnBadField(t *testing.T) { fields := []TxnField{ApplicationArgs, Accounts} for _, field := range fields { source := fmt.Sprintf("txn %s 0", field.String()) - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, txnaOpcode, program[1]) program[1] = txnOpcode @@ -1050,7 +1110,7 @@ func TestGtxnBadField(t *testing.T) { fields := []TxnField{ApplicationArgs, Accounts} for _, field := range fields { source := fmt.Sprintf("txn %s 0", field.String()) - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) require.Equal(t, txnaOpcode, program[1]) program[1] = txnOpcode @@ -1083,7 +1143,7 @@ func TestGlobalBadField(t *testing.T) { func TestArg(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`arg 0 arg 1 @@ -1155,6 +1215,10 @@ global LatestTimestamp int 0 > && +global CurrentApplicationID +int 42 +== +&& ` func TestGlobal(t *testing.T) { @@ -1169,7 +1233,7 @@ func TestGlobal(t *testing.T) { 0: {GroupSize, globalV1TestProgram, Eval, Check}, 1: {GroupSize, globalV1TestProgram, Eval, Check}, 2: { - LatestTimestamp, globalV1TestProgram + globalV2TestProgram, + CurrentApplicationID, globalV1TestProgram + globalV2TestProgram, func(p []byte, ep EvalParams) (bool, error) { pass, _, err := EvalStateful(p, ep) return pass, err @@ -1178,7 +1242,8 @@ func TestGlobal(t *testing.T) { }, } ledger := makeTestLedger(nil) - for v := uint64(0); v <= AssemblerDefaultVersion; v++ { + ledger.appID = 42 + for v := uint64(0); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { last := tests[v].lastField testProgram := tests[v].program @@ -1226,7 +1291,7 @@ func TestGlobal(t *testing.T) { func TestTypeEnum(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { ttypes := []protocol.TxType{ protocol.PaymentTx, @@ -1304,7 +1369,7 @@ func TestOnCompletionConstants(t *testing.T) { // check constants matching to their values ep := defaultEvalParams(nil, nil) - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { for i := 0; i < last; i++ { oc := OnCompletionConstType(i) @@ -1433,7 +1498,7 @@ byte 0x706179 == && txn NumAppArgs -int 1 +int 8 == && txna Accounts 0 @@ -1458,6 +1523,70 @@ sha512_256 arg 10 == && +txn RekeyTo +txna ApplicationArgs 1 +== +&& +txn ConfigAsset +int 33 +== +&& +txn ConfigAssetTotal +int 100 +== +&& +txn ConfigAssetDecimals +int 2 +== +&& +txn ConfigAssetDefaultFrozen +int 1 +== +&& +txn ConfigAssetUnitName +byte "tok" +== +&& +txn ConfigAssetName +byte "a_super_coin" +== +&& +txn ConfigAssetURL +byte "http://algorand.com" +== +&& +txn ConfigAssetMetadataHash +txna ApplicationArgs 2 +== +&& +txn ConfigAssetManager +txna ApplicationArgs 3 +== +&& +txn ConfigAssetReserve +txna ApplicationArgs 4 +== +&& +txn ConfigAssetFreeze +txna ApplicationArgs 5 +== +&& +txn ConfigAssetClawback +txna ApplicationArgs 6 +== +&& +txn FreezeAsset +int 34 +== +&& +txn FreezeAssetAccount +txna ApplicationArgs 7 +== +&& +txn FreezeAssetFrozen +int 1 +== +&& ` func makeSampleTxn() transactions.SignedTxn { @@ -1486,8 +1615,40 @@ func makeSampleTxn() transactions.SignedTxn { txn.Txn.ApplicationID = basics.AppIndex(123) txn.Txn.Accounts = make([]basics.Address, 1) txn.Txn.Accounts[0] = txn.Txn.Receiver - txn.Txn.ApplicationArgs = make([][]byte, 1) - txn.Txn.ApplicationArgs[0] = []byte(protocol.PaymentTx) + rekeyToAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui05") + metadata := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeuiHH") + managerAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui06") + reserveAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui07") + freezeAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui08") + clawbackAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui09") + freezeAccAddr := []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui10") + txn.Txn.ApplicationArgs = [][]byte{ + []byte(protocol.PaymentTx), + rekeyToAddr, + metadata, + managerAddr, + reserveAddr, + freezeAddr, + clawbackAddr, + freezeAccAddr, + } + copy(txn.Txn.RekeyTo[:], rekeyToAddr) + txn.Txn.ConfigAsset = 33 + txn.Txn.AssetParams.Total = 100 + txn.Txn.AssetParams.Decimals = 2 + txn.Txn.AssetParams.DefaultFrozen = true + txn.Txn.AssetParams.UnitName = "tok" + txn.Txn.AssetParams.AssetName = "a_super_coin" + txn.Txn.AssetParams.URL = "http://algorand.com" + txn.Txn.AssetParams.UnitName = "tok" + copy(txn.Txn.AssetParams.MetadataHash[:], metadata) + copy(txn.Txn.AssetParams.Manager[:], managerAddr) + copy(txn.Txn.AssetParams.Reserve[:], reserveAddr) + copy(txn.Txn.AssetParams.Freeze[:], freezeAddr) + copy(txn.Txn.AssetParams.Clawback[:], clawbackAddr) + txn.Txn.FreezeAsset = 34 + copy(txn.Txn.FreezeAccount[:], freezeAccAddr) + txn.Txn.AssetFrozen = true return txn } @@ -1532,6 +1693,10 @@ func TestTxn(t *testing.T) { txn.Txn.ApprovalProgram = program txn.Txn.ClearStateProgram = clearProgram txn.Lsig.Logic = program + // RekeyTo not allowed in TEAL v1 + if v < rekeyingEnabledVersion { + txn.Txn.RekeyTo = basics.Address{} + } txid := txn.Txn.ID() programHash := HashProgram(program) clearProgramHash := HashProgram(clearProgram) @@ -1694,7 +1859,7 @@ byte 0x706179 == && gtxn 0 NumAppArgs -int 1 +int 8 == && gtxna 0 Accounts 0 @@ -1726,6 +1891,10 @@ int 1 require.True(t, cost < 1000) txn := makeSampleTxn() txgroup := makeSampleTxnGroup(txn) + // RekeyTo not allowed in TEAL v1 + if v < rekeyingEnabledVersion { + txn.Txn.RekeyTo = basics.Address{} + } txn.Lsig.Logic = program txn.Lsig.Args = [][]byte{ txn.Txn.Sender[:], @@ -1755,7 +1924,7 @@ func TestTxna(t *testing.T) { txna ApplicationArgs 0 == ` - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) var txn transactions.SignedTxn txn.Txn.Accounts = make([]basics.Address, 1) @@ -1805,7 +1974,7 @@ txna ApplicationArgs 0 txn Sender == ` - program2, err := AssembleString(source) + program2, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) var txn2 transactions.SignedTxn copy(txn2.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) @@ -1818,7 +1987,7 @@ txn Sender source = `gtxna 0 Accounts 1 txna ApplicationArgs 0 ==` - program, err = AssembleString(source) + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Eval(program, ep) require.NoError(t, err) @@ -1859,7 +2028,7 @@ txna ApplicationArgs 0 txn Sender == ` - program3, err := AssembleString(source) + program3, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) var txn3 transactions.SignedTxn copy(txn2.Txn.Sender[:], []byte("aoeuiaoeuiaoeuiaoeuiaoeuiaoeui00")) @@ -1880,7 +2049,7 @@ btoi int 0 == ` - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) var txn transactions.SignedTxn @@ -1904,7 +2073,7 @@ int 0 global ZeroAddress == ` - program2, err := AssembleString(source2) + program2, err := AssembleStringWithVersion(source2, AssemblerMaxVersion) require.NoError(t, err) var txn2 transactions.SignedTxn @@ -1927,7 +2096,7 @@ global ZeroAddress func TestBitOps(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 0x17 int 0x3e @@ -2114,7 +2283,7 @@ len`, 2) func TestLoadStore(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 37 int 37 @@ -2151,7 +2320,7 @@ func assembleStringWithTrace(t testing.TB, text string, version uint64) ([]byte, sr := strings.NewReader(text) sb := strings.Builder{} ops := OpStream{Trace: &sb, Version: version} - err := ops.Assemble(sr) + err := ops.assemble(sr) if err != nil { t.Log(sb.String()) return nil, err @@ -2172,7 +2341,7 @@ load 42 + int 5 ==` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := assembleStringWithTrace(t, progText, v) require.NoError(t, err) @@ -2253,7 +2422,7 @@ byte 0xf00d func TestCompares(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(testCompareProgramText, v) require.NoError(t, err) @@ -2284,7 +2453,7 @@ func TestKeccak256(t *testing.T) { keccak256 byte 0xc195eca25a6f4c82bfba0287082ddb0d602ae9230f9cf1f1a40b68f8e2c41567 ==` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(progText, v) require.NoError(t, err) @@ -2319,7 +2488,7 @@ sha512_256 byte 0x98D2C31612EA500279B6753E5F6E780CA63EBA8274049664DAD66A2565ED1D2A ==` - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(progText, v) require.NoError(t, err) @@ -2349,7 +2518,7 @@ func isNotPanic(t *testing.T, err error) { func TestStackUnderflow(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) program = append(program, 0x08) // + @@ -2371,7 +2540,7 @@ func TestStackUnderflow(t *testing.T) { func TestWrongStackTypeRuntime(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) require.NoError(t, err) @@ -2393,7 +2562,7 @@ func TestWrongStackTypeRuntime(t *testing.T) { func TestEqMismatch(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`byte 0x1234 int 1`, v) @@ -2416,7 +2585,7 @@ int 1`, v) func TestNeqMismatch(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`byte 0x1234 int 1`, v) @@ -2439,7 +2608,7 @@ int 1`, v) func TestWrongStackTypeRuntime2(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`byte 0x1234 int 1`, v) @@ -2462,7 +2631,7 @@ int 1`, v) func TestIllegalOp(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) require.NoError(t, err) @@ -2489,7 +2658,7 @@ func TestIllegalOp(t *testing.T) { func TestShortProgram(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 bnz done @@ -2529,7 +2698,7 @@ done:`, 2) } func TestShortBytecblock(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { fullprogram, err := AssembleStringWithVersion(`bytecblock 0x123456 0xababcdcd "test"`, v) require.NoError(t, err) @@ -2594,7 +2763,7 @@ func checkPanic(cx *evalContext) int { func TestPanic(t *testing.T) { log := logging.TestingLog(t) - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1`, v) require.NoError(t, err) @@ -2692,7 +2861,7 @@ func TestProgramProtoForbidden(t *testing.T) { func TestMisalignedBranch(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 bnz done @@ -2719,7 +2888,7 @@ int 1`, v) func TestBranchTooFar(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 bnz done @@ -2746,7 +2915,7 @@ int 1`, v) func TestBranchTooLarge(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 bnz done @@ -2783,7 +2952,7 @@ int 1 for _, line := range branches { t.Run(fmt.Sprintf("branch=%s", line), func(t *testing.T) { source := fmt.Sprintf(template, line) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) program[7] = 0xff // clobber the branch offset program[8] = 0xff // clobber the branch offset @@ -3075,7 +3244,7 @@ int 142791994204213819 ` func benchmarkBasicProgram(b *testing.B, source string) { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(b, err) cost, err := Check(program, defaultEvalParams(nil, nil)) require.NoError(b, err) @@ -3100,7 +3269,7 @@ func benchmarkBasicProgram(b *testing.B, source string) { } func benchmarkExpensiveProgram(b *testing.B, source string) { - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(b, err) cost, err := Check(program, defaultEvalParams(nil, nil)) require.NoError(b, err) @@ -3194,7 +3363,7 @@ func TestEd25519verify(t *testing.T) { pk := basics.Address(c.SignatureVerifier) pkStr := pk.String() - for v := uint64(1); v <= AssemblerDefaultVersion; v++ { + for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(fmt.Sprintf(`arg 0 arg 1 @@ -3254,10 +3423,10 @@ func BenchmarkEd25519Verifyx1(b *testing.B) { secret := crypto.GenerateSignatureSecrets(s) pk := basics.Address(secret.SignatureVerifier) pkStr := pk.String() - program, err := AssembleString(fmt.Sprintf(`arg 0 + program, err := AssembleStringWithVersion(fmt.Sprintf(`arg 0 arg 1 addr %s -ed25519verify`, pkStr)) +ed25519verify`, pkStr), AssemblerMaxVersion) require.NoError(b, err) programs = append(programs, program) sig := secret.SignBytes(buffer[:]) @@ -3297,7 +3466,7 @@ func BenchmarkCheckx5(b *testing.B) { programs := make([][]byte, len(sourcePrograms)) var err error for i, text := range sourcePrograms { - programs[i], err = AssembleString(text) + programs[i], err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(b, err) } b.ResetTimer() @@ -3334,7 +3503,7 @@ func TestEvalVersions(t *testing.T) { txna ApplicationArgs 0 pop ` - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) @@ -3364,7 +3533,7 @@ intc_0 intc_0 + ` - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) ep := defaultEvalParams(nil, nil) @@ -3418,14 +3587,14 @@ int 1 ` ep := defaultEvalParams(nil, nil) - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, err := Eval(program, ep) require.NoError(t, err) require.True(t, pass) text = `dup2` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, err = Eval(program, ep) require.Error(t, err) @@ -3434,7 +3603,7 @@ int 1 text = `int 1 dup2 ` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, err = Eval(program, ep) require.Error(t, err) @@ -3450,7 +3619,7 @@ byte b64(Zm9vIGJhcg==) ` ep := defaultEvalParams(nil, nil) - program, err := AssembleString(text) + program, err := AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, err := Eval(program, ep) require.NoError(t, err) @@ -3460,7 +3629,7 @@ byte b64(Zm9vIGJhcg==) byte b64(Zm9vIGJhciAvLyBub3QgYSBjb21tZW50) == ` - program, err = AssembleString(text) + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) pass, err = Eval(program, ep) require.NoError(t, err) @@ -3490,6 +3659,7 @@ func TestAllowedOpcodesV2(t *testing.T) { "bz": "bz l\nl:", "b": "b l\nl:", "return": "int 1\nreturn", + "addw": "int 0\nint 1\naddw", "dup2": "dup2", "concat": "byte 0x41\ndup\nconcat", "substring": "byte 0x41\nsubstring 0 1", @@ -3523,7 +3693,7 @@ func TestAllowedOpcodesV2(t *testing.T) { if spec.Version > 1 && !excluded[spec.Name] { source, ok := tests[spec.Name] require.True(t, ok, fmt.Sprintf("Missed opcode in the test: %s", spec.Name)) - program, err := AssembleString(source) + program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err, source) // all opcodes allowed in stateful mode so use CheckStateful/EvalStateful _, err = CheckStateful(program, ep) @@ -3561,3 +3731,27 @@ func TestAllowedOpcodesV2(t *testing.T) { } require.Equal(t, len(tests), cnt) } + +func TestRekeyFailsOnOldVersion(t *testing.T) { + t.Parallel() + for v := uint64(0); v < rekeyingEnabledVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(`int 1`, v) + require.NoError(t, err) + var txn transactions.SignedTxn + txn.Lsig.Logic = program + txn.Txn.RekeyTo = basics.Address{1, 2, 3, 4} + sb := strings.Builder{} + proto := defaultEvalProto() + ep := defaultEvalParams(&sb, &txn) + ep.Proto = &proto + _, err = Check(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "nonzero RekeyTo field") + pass, err := Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "nonzero RekeyTo field") + require.False(t, pass) + }) + } +} diff --git a/data/transactions/logic/fields.go b/data/transactions/logic/fields.go index 9158df9659..69fd5a44f4 100644 --- a/data/transactions/logic/fields.go +++ b/data/transactions/logic/fields.go @@ -91,6 +91,38 @@ const ( ApprovalProgram // ClearStateProgram []byte ClearStateProgram + // RekeyTo basics.Address + RekeyTo + // ConfigAsset basics.AssetIndex + ConfigAsset + // ConfigAssetTotal AssetParams.Total + ConfigAssetTotal + // ConfigAssetDecimals AssetParams.Decimals + ConfigAssetDecimals + // ConfigAssetDefaultFrozen AssetParams.AssetDefaultFrozen + ConfigAssetDefaultFrozen + // ConfigAssetUnitName AssetParams.UnitName + ConfigAssetUnitName + // ConfigAssetName AssetParams.AssetName + ConfigAssetName + // ConfigAssetURL AssetParams.URL + ConfigAssetURL + // ConfigAssetMetadataHash AssetParams.MetadataHash + ConfigAssetMetadataHash + // ConfigAssetManager AssetParams.Manager + ConfigAssetManager + // ConfigAssetReserve AssetParams.Reserve + ConfigAssetReserve + // ConfigAssetFreeze AssetParams.Freeze + ConfigAssetFreeze + // ConfigAssetClawback AssetParams.Clawback + ConfigAssetClawback + //FreezeAsset basics.AssetIndex + FreezeAsset + // FreezeAssetAccount basics.Address + FreezeAssetAccount + // FreezeAssetFrozen bool + FreezeAssetFrozen invalidTxnField // fence for some setup that loops from Sender..invalidTxnField ) @@ -153,6 +185,37 @@ var txnFieldSpecs = []txnFieldSpec{ {NumAccounts, StackUint64, 2}, {ApprovalProgram, StackBytes, 2}, {ClearStateProgram, StackBytes, 2}, + {RekeyTo, StackBytes, 2}, + {ConfigAsset, StackUint64, 2}, + {ConfigAssetTotal, StackUint64, 2}, + {ConfigAssetDecimals, StackUint64, 2}, + {ConfigAssetDefaultFrozen, StackUint64, 2}, + {ConfigAssetUnitName, StackBytes, 2}, + {ConfigAssetName, StackBytes, 2}, + {ConfigAssetURL, StackBytes, 2}, + {ConfigAssetMetadataHash, StackBytes, 2}, + {ConfigAssetManager, StackBytes, 2}, + {ConfigAssetReserve, StackBytes, 2}, + {ConfigAssetFreeze, StackBytes, 2}, + {ConfigAssetClawback, StackBytes, 2}, + {FreezeAsset, StackUint64, 2}, + {FreezeAssetAccount, StackBytes, 2}, + {FreezeAssetFrozen, StackUint64, 2}, +} + +// TxnaFieldNames are arguments to the 'txna' opcode +// It is a subset of txn transaction fields so initialized here in-place +var TxnaFieldNames = []string{ApplicationArgs.String(), Accounts.String()} + +// TxnaFieldTypes is StackBytes or StackUint64 parallel to TxnFieldNames +var TxnaFieldTypes = []StackType{ + txnaFieldSpecByField[ApplicationArgs].ftype, + txnaFieldSpecByField[Accounts].ftype, +} + +var txnaFieldSpecByField = map[TxnField]txnFieldSpec{ + ApplicationArgs: {ApplicationArgs, StackBytes, 2}, + Accounts: {Accounts, StackBytes, 2}, } // TxnTypeNames is the values of Txn.Type in enum order @@ -218,6 +281,8 @@ const ( Round // LatestTimestamp uint64 LatestTimestamp + // CurrentApplicationID uint64 + CurrentApplicationID invalidGlobalField ) @@ -244,6 +309,7 @@ var globalFieldSpecs = []globalFieldSpec{ {LogicSigVersion, StackUint64, modeAny, 2}, {Round, StackUint64, runModeApplication, 2}, {LatestTimestamp, StackUint64, runModeApplication, 2}, + {CurrentApplicationID, StackUint64, runModeApplication, 2}, } // GlobalFieldSpecByField maps GlobalField to spec @@ -301,8 +367,8 @@ const ( AssetDefaultFrozen // AssetUnitName AssetParams.UnitName AssetUnitName - // AssetAssetName AssetParams.AssetName - AssetAssetName + // AssetName AssetParams.AssetName + AssetName // AssetURL AssetParams.URL AssetURL // AssetMetadataHash AssetParams.MetadataHash @@ -331,7 +397,7 @@ var assetParamsFieldTypeList = []assetParamsFieldType{ {AssetDecimals, StackUint64}, {AssetDefaultFrozen, StackUint64}, {AssetUnitName, StackBytes}, - {AssetAssetName, StackBytes}, + {AssetName, StackBytes}, {AssetURL, StackBytes}, {AssetMetadataHash, StackBytes}, {AssetManager, StackBytes}, diff --git a/data/transactions/logic/fields_string.go b/data/transactions/logic/fields_string.go index 0617b686e3..046b5a55a9 100644 --- a/data/transactions/logic/fields_string.go +++ b/data/transactions/logic/fields_string.go @@ -40,12 +40,28 @@ func _() { _ = x[NumAccounts-29] _ = x[ApprovalProgram-30] _ = x[ClearStateProgram-31] - _ = x[invalidTxnField-32] + _ = x[RekeyTo-32] + _ = x[ConfigAsset-33] + _ = x[ConfigAssetTotal-34] + _ = x[ConfigAssetDecimals-35] + _ = x[ConfigAssetDefaultFrozen-36] + _ = x[ConfigAssetUnitName-37] + _ = x[ConfigAssetName-38] + _ = x[ConfigAssetURL-39] + _ = x[ConfigAssetMetadataHash-40] + _ = x[ConfigAssetManager-41] + _ = x[ConfigAssetReserve-42] + _ = x[ConfigAssetFreeze-43] + _ = x[ConfigAssetClawback-44] + _ = x[FreezeAsset-45] + _ = x[FreezeAssetAccount-46] + _ = x[FreezeAssetFrozen-47] + _ = x[invalidTxnField-48] } -const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDApplicationIDOnCompletionApplicationArgsNumAppArgsAccountsNumAccountsApprovalProgramClearStatePrograminvalidTxnField" +const _TxnField_name = "SenderFeeFirstValidFirstValidTimeLastValidNoteLeaseReceiverAmountCloseRemainderToVotePKSelectionPKVoteFirstVoteLastVoteKeyDilutionTypeTypeEnumXferAssetAssetAmountAssetSenderAssetReceiverAssetCloseToGroupIndexTxIDApplicationIDOnCompletionApplicationArgsNumAppArgsAccountsNumAccountsApprovalProgramClearStateProgramRekeyToConfigAssetConfigAssetTotalConfigAssetDecimalsConfigAssetDefaultFrozenConfigAssetUnitNameConfigAssetNameConfigAssetURLConfigAssetMetadataHashConfigAssetManagerConfigAssetReserveConfigAssetFreezeConfigAssetClawbackFreezeAssetFreezeAssetAccountFreezeAssetFrozeninvalidTxnField" -var _TxnField_index = [...]uint16{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 225, 237, 252, 262, 270, 281, 296, 313, 328} +var _TxnField_index = [...]uint16{0, 6, 9, 19, 33, 42, 46, 51, 59, 65, 81, 87, 98, 107, 115, 130, 134, 142, 151, 162, 173, 186, 198, 208, 212, 225, 237, 252, 262, 270, 281, 296, 313, 320, 331, 347, 366, 390, 409, 424, 438, 461, 479, 497, 514, 533, 544, 562, 579, 594} func (i TxnField) String() string { if i < 0 || i >= TxnField(len(_TxnField_index)-1) { @@ -65,12 +81,13 @@ func _() { _ = x[LogicSigVersion-5] _ = x[Round-6] _ = x[LatestTimestamp-7] - _ = x[invalidGlobalField-8] + _ = x[CurrentApplicationID-8] + _ = x[invalidGlobalField-9] } -const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeLogicSigVersionRoundLatestTimestampinvalidGlobalField" +const _GlobalField_name = "MinTxnFeeMinBalanceMaxTxnLifeZeroAddressGroupSizeLogicSigVersionRoundLatestTimestampCurrentApplicationIDinvalidGlobalField" -var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 64, 69, 84, 102} +var _GlobalField_index = [...]uint8{0, 9, 19, 29, 40, 49, 64, 69, 84, 104, 122} func (i GlobalField) String() string { if i < 0 || i >= GlobalField(len(_GlobalField_index)-1) { @@ -86,7 +103,7 @@ func _() { _ = x[AssetDecimals-1] _ = x[AssetDefaultFrozen-2] _ = x[AssetUnitName-3] - _ = x[AssetAssetName-4] + _ = x[AssetName-4] _ = x[AssetURL-5] _ = x[AssetMetadataHash-6] _ = x[AssetManager-7] @@ -96,9 +113,9 @@ func _() { _ = x[invalidAssetParamsField-11] } -const _AssetParamsField_name = "AssetTotalAssetDecimalsAssetDefaultFrozenAssetUnitNameAssetAssetNameAssetURLAssetMetadataHashAssetManagerAssetReserveAssetFreezeAssetClawbackinvalidAssetParamsField" +const _AssetParamsField_name = "AssetTotalAssetDecimalsAssetDefaultFrozenAssetUnitNameAssetNameAssetURLAssetMetadataHashAssetManagerAssetReserveAssetFreezeAssetClawbackinvalidAssetParamsField" -var _AssetParamsField_index = [...]uint8{0, 10, 23, 41, 54, 68, 76, 93, 105, 117, 128, 141, 164} +var _AssetParamsField_index = [...]uint8{0, 10, 23, 41, 54, 63, 71, 88, 100, 112, 123, 136, 159} func (i AssetParamsField) String() string { if i < 0 || i >= AssetParamsField(len(_AssetParamsField_index)-1) { diff --git a/data/transactions/logic/kvcow.go b/data/transactions/logic/kvcow.go index 011970b986..e9f577a407 100644 --- a/data/transactions/logic/kvcow.go +++ b/data/transactions/logic/kvcow.go @@ -17,19 +17,53 @@ package logic import ( + "fmt" + + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" ) type keyValueCow struct { base basics.TealKeyValue delta basics.StateDelta + + maxSchema basics.StateSchema + calcSchema basics.StateSchema + + maxKeyLen int + maxValLen int } -func makeKeyValueCow(base basics.TealKeyValue, delta basics.StateDelta) *keyValueCow { +func makeKeyValueCow(base basics.TealKeyValue, delta basics.StateDelta, maxSchema basics.StateSchema, proto *config.ConsensusParams) (*keyValueCow, error) { var kvc keyValueCow + var err error + + if proto == nil { + return nil, fmt.Errorf("got nil consensus params in kvcow") + } + + if delta == nil { + return nil, fmt.Errorf("got nil delta in kvcow") + } + kvc.base = base kvc.delta = delta - return &kvc + kvc.maxSchema = maxSchema + kvc.maxKeyLen = proto.MaxAppKeyLen + kvc.maxValLen = proto.MaxAppBytesValueLen + + kvc.calcSchema, err = base.ToStateSchema() + if err != nil { + return nil, err + } + + // Check that the backing map is compliant with the consensus params + err = kvc.checkSchema() + if err != nil { + return nil, err + } + + return &kvc, nil } func (kvc *keyValueCow) read(key string) (value basics.TealValue, ok bool) { @@ -45,7 +79,20 @@ func (kvc *keyValueCow) read(key string) (value basics.TealValue, ok bool) { return value, ok } -func (kvc *keyValueCow) write(key string, value basics.TealValue) { +func (kvc *keyValueCow) write(key string, value basics.TealValue) error { + // Enforce maximum key length + if len(key) > kvc.maxKeyLen { + return fmt.Errorf("key too long: length was %d, maximum is %d", len(key), kvc.maxKeyLen) + } + + // Enforce maximum value length + if value.Type == basics.TealBytesType && len(value.Bytes) > kvc.maxValLen { + return fmt.Errorf("value too long for key 0x%x: length was %d, maximum is %d", key, len(value.Bytes), kvc.maxValLen) + } + + // Keep track of old value for updating counts + beforeValue, beforeOk := kvc.read(key) + // If the value being written is identical to the underlying key/value, // then ensure there is no delta entry for the key. baseValue, ok := kvc.base[key] @@ -55,9 +102,20 @@ func (kvc *keyValueCow) write(key string, value basics.TealValue) { // Otherwise, update the delta with the new value. kvc.delta[key] = value.ToValueDelta() } + + // Keep track of new value for updating counts + afterValue, afterOk := kvc.read(key) + err := kvc.updateSchema(beforeValue, beforeOk, afterValue, afterOk) + if err != nil { + return err + } + return kvc.checkSchema() } -func (kvc *keyValueCow) del(key string) { +func (kvc *keyValueCow) del(key string) error { + // Keep track of old value for updating counts + beforeValue, beforeOk := kvc.read(key) + _, ok := kvc.base[key] if ok { // If the key already exists in the underlying key/value, @@ -70,4 +128,52 @@ func (kvc *keyValueCow) del(key string) { // don't include a delta entry for its deletion. delete(kvc.delta, key) } + + // Keep track of new value for updating counts. Technically a delete + // can never cause a schema violation, but for let's return an error + // type for functions that can modify the cow + afterValue, afterOk := kvc.read(key) + err := kvc.updateSchema(beforeValue, beforeOk, afterValue, afterOk) + if err != nil { + return err + } + return kvc.checkSchema() +} + +func (kvc *keyValueCow) updateSchema(bv basics.TealValue, bok bool, av basics.TealValue, aok bool) error { + // If the value existed before, decrement the count of the old type. + if bok { + switch bv.Type { + case basics.TealBytesType: + kvc.calcSchema.NumByteSlice-- + case basics.TealUintType: + kvc.calcSchema.NumUint-- + default: + return fmt.Errorf("unknown before type: %v", bv.Type) + } + } + + // If the value exists now, increment the count of the new type. + if aok { + switch av.Type { + case basics.TealBytesType: + kvc.calcSchema.NumByteSlice++ + case basics.TealUintType: + kvc.calcSchema.NumUint++ + default: + return fmt.Errorf("unknown after type: %v", av.Type) + } + } + return nil +} + +func (kvc *keyValueCow) checkSchema() error { + // Check against the max schema + if kvc.calcSchema.NumUint > kvc.maxSchema.NumUint { + return fmt.Errorf("store integer count %d exceeds schema integer count %d", kvc.calcSchema.NumUint, kvc.maxSchema.NumUint) + } + if kvc.calcSchema.NumByteSlice > kvc.maxSchema.NumByteSlice { + return fmt.Errorf("store bytes count %d exceeds schema bytes count %d", kvc.calcSchema.NumByteSlice, kvc.maxSchema.NumByteSlice) + } + return nil } diff --git a/data/transactions/logic/kvcow_test.go b/data/transactions/logic/kvcow_test.go new file mode 100644 index 0000000000..4bf3e02b6e --- /dev/null +++ b/data/transactions/logic/kvcow_test.go @@ -0,0 +1,192 @@ +// 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 . + +package logic + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" +) + +var byteVal = basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "hello", +} + +var byteVal2 = basics.TealValue{ + Type: basics.TealBytesType, + Bytes: "goodbye", +} + +var uintVal = basics.TealValue{ + Type: basics.TealUintType, + Uint: 1234, +} + +func TestKeyValueCowReadWriteDelete(t *testing.T) { + base := make(basics.TealKeyValue) + delta := make(basics.StateDelta) + maxSchema := basics.StateSchema{ + NumByteSlice: 100, + NumUint: 100, + } + proto := &config.ConsensusParams{ + MaxAppKeyLen: 100, + MaxAppBytesValueLen: 100, + } + + base["hi"] = byteVal + + kvCow, err := makeKeyValueCow(base, delta, maxSchema, proto) + require.NoError(t, err) + require.NotNil(t, kvCow) + + // Check that we can read through to the base map + v, ok := kvCow.read("hi") + require.True(t, ok) + require.Equal(t, v, base["hi"]) + + // Check that we can delete through to base map + err = kvCow.del("hi") + require.NoError(t, err) + require.Equal(t, 1, len(kvCow.delta)) + _, ok = kvCow.read("hi") + require.False(t, ok) + + // Check that writing the same value as backing map yields no delta + err = kvCow.write("hi", byteVal) + require.NoError(t, err) + require.Equal(t, 0, len(kvCow.delta)) + + // Check that deleting a key that does not exist yields no delta + err = kvCow.del("bye") + require.NoError(t, err) + require.Equal(t, 0, len(kvCow.delta)) +} + +func TestKeyValueCowSchemaCounts(t *testing.T) { + base := make(basics.TealKeyValue) + delta := make(basics.StateDelta) + maxSchema := basics.StateSchema{ + NumByteSlice: 4, + NumUint: 4, + } + proto := &config.ConsensusParams{ + MaxAppKeyLen: 100, + MaxAppBytesValueLen: 100, + } + + base["a"] = byteVal + base["b"] = uintVal + base["c"] = uintVal + + kvCow, err := makeKeyValueCow(base, delta, maxSchema, proto) + require.NoError(t, err) + require.NotNil(t, kvCow) + + // Check that the initial count is correct + require.Equal(t, uint64(2), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Write a new integer and check counts + err = kvCow.write("d", uintVal) + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Delete base byte slice and check counts + err = kvCow.del("a") + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(0), kvCow.calcSchema.NumByteSlice) + + // Delete again, counts shouldn't change + err = kvCow.del("a") + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(0), kvCow.calcSchema.NumByteSlice) + + // Write an integer over deleted byte slice + err = kvCow.write("a", uintVal) + require.NoError(t, err) + require.Equal(t, uint64(4), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(0), kvCow.calcSchema.NumByteSlice) + + // Overwrite an integer with a byte slice + err = kvCow.write("c", byteVal) + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Overwrite a byte slice with a different byte slice + err = kvCow.write("c", byteVal2) + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Overwrite a byte slice with the same byte slice + err = kvCow.write("c", byteVal2) + require.NoError(t, err) + require.Equal(t, uint64(3), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Write two more integers, second should fail with limit + err = kvCow.write("e", uintVal) + require.NoError(t, err) + require.Equal(t, uint64(4), kvCow.calcSchema.NumUint) + require.Equal(t, uint64(1), kvCow.calcSchema.NumByteSlice) + + // Write two more integers, second should fail with limit + err = kvCow.write("f", uintVal) + require.Error(t, err) + require.Contains(t, err.Error(), "integer count 5 exceeds") +} + +func TestKeyValueCowLengthLimits(t *testing.T) { + base := make(basics.TealKeyValue) + delta := make(basics.StateDelta) + maxSchema := basics.StateSchema{ + NumByteSlice: 4, + NumUint: 4, + } + proto := &config.ConsensusParams{ + MaxAppKeyLen: 5, + MaxAppBytesValueLen: 5, + } + + kvCow, err := makeKeyValueCow(base, delta, maxSchema, proto) + require.NoError(t, err) + require.NotNil(t, kvCow) + + // Writing a long key should fail + err = kvCow.write("aaaaaa", uintVal) + require.Error(t, err) + require.Contains(t, err.Error(), "key too long") + + // Writing a long value should fail + err = kvCow.write("a", byteVal2) + require.Error(t, err) + require.Contains(t, err.Error(), "value too long") + + // Writing both too long should fial + err = kvCow.write("aaaaaa", byteVal2) + require.Error(t, err) + require.Contains(t, err.Error(), "too long") +} diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index 3228b32188..a2dbc29ef5 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -23,6 +23,11 @@ import ( // LogicVersion defines default assembler and max eval versions const LogicVersion = 2 +// rekeyingEnabledVersion is the version of TEAL where RekeyTo functionality +// was enabled. This is important to remember so that old TEAL accounts cannot +// be maliciously or accidentally rekeyed. +const rekeyingEnabledVersion = 2 + // opSize records the length in bytes for an op that is constant-length but not length 1 type opSize struct { cost int @@ -97,6 +102,7 @@ var OpSpecs = []OpSpec{ {0x1b, "^", opBitXor, asmDefault, disDefault, twoInts, oneInt, 1, modeAny, opSizeDefault}, {0x1c, "~", opBitNot, asmDefault, disDefault, oneInt, oneInt, 1, modeAny, opSizeDefault}, {0x1d, "mulw", opMulw, asmDefault, disDefault, twoInts, twoInts, 1, modeAny, opSizeDefault}, + {0x1e, "addw", opAddw, asmDefault, disDefault, twoInts, twoInts, 2, modeAny, opSizeDefault}, {0x20, "intcblock", opIntConstBlock, assembleIntCBlock, disIntcblock, nil, nil, 1, modeAny, opSize{1, 0, checkIntConstBlock}}, {0x21, "intc", opIntConstLoad, assembleIntC, disIntc, nil, oneInt, 1, modeAny, opSize{1, 2, nil}}, diff --git a/data/transactions/logic/opcodes_test.go b/data/transactions/logic/opcodes_test.go index 58d6c78659..7364cd7197 100644 --- a/data/transactions/logic/opcodes_test.go +++ b/data/transactions/logic/opcodes_test.go @@ -144,7 +144,7 @@ func TestOpcodesVersioningV2(t *testing.T) { require.Equal(t, cntv2, len(opsByName[2])) // hardcode and ensure amount of new v2 opcodes - newOpcodes := 21 + newOpcodes := 22 overwritten := 5 // sha256, keccak256, sha512_256, txn, gtxn require.Equal(t, newOpcodes+overwritten, cntAdded) diff --git a/data/transactions/payment_test.go b/data/transactions/payment_test.go index 374daad486..e6e9435267 100644 --- a/data/transactions/payment_test.go +++ b/data/transactions/payment_test.go @@ -80,7 +80,7 @@ func (balances mockBalances) Round() basics.Round { return basics.Round(8675309) } -func (balances mockBalances) PutWithCreatables(basics.BalanceRecord, []basics.CreatableLocator, []basics.CreatableLocator) error { +func (balances mockBalances) PutWithCreatable(basics.BalanceRecord, *basics.CreatableLocator, *basics.CreatableLocator) error { return nil } @@ -88,11 +88,7 @@ func (balances mockBalances) Get(basics.Address, bool) (basics.BalanceRecord, er return basics.BalanceRecord{}, nil } -func (balances mockBalances) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { - return basics.Address{}, nil -} - -func (balances mockBalances) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { +func (balances mockBalances) GetCreator(idx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { return basics.Address{}, true, nil } diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 1c15871fb8..88539668f6 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -57,16 +57,11 @@ type Balances interface { Put(basics.BalanceRecord) error - // PutWithCreatables is like Put, but should be used when creating or deleting an asset or application. - PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error + // PutWithCreatable is like Put, but should be used when creating or deleting an asset or application. + PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error - // GetAssetCreator gets the address of the account whose balance record - // contains the asset params - GetAssetCreator(aidx basics.AssetIndex) (basics.Address, error) - - // GetAppCreator gets the address of the account whose balance record - // contains the app params - GetAppCreator(aidx basics.AppIndex) (basics.Address, bool, error) + // GetCreator gets the address of the account that created a given creatable + GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) // Move MicroAlgos from one account to another, doing all necessary overflow checking (convenience method) // TODO: Does this need to be part of the balances interface, or can it just be implemented here as a function that calls Put and Get? @@ -83,7 +78,7 @@ type Balances interface { type StateEvaluator interface { Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) Check(program []byte) (cost int, err error) - InitLedger(balances Balances, acctWhitelist []basics.Address, appGlobalWhitelist []basics.AppIndex, appIdx basics.AppIndex) error + InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error } // Header captures the fields common to every transaction type. diff --git a/go.mod b/go.mod index e5445e92d6..6f2a8c3622 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fatih/color v1.7.0 github.com/fortytw2/leaktest v1.3.0 // indirect github.com/gen2brain/beeep v0.0.0-20180718162406-4e430518395f - github.com/getkin/kin-openapi v0.9.0 + github.com/getkin/kin-openapi v0.14.0 github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/godbus/dbus v0.0.0-20181101234600-2ff6f7ffd60f // indirect github.com/gofrs/flock v0.7.0 @@ -44,16 +44,21 @@ require ( github.com/sirupsen/logrus v1.0.5 github.com/spf13/cobra v0.0.3 github.com/spf13/pflag v1.0.3 // indirect - github.com/stretchr/testify v1.5.1 + github.com/stretchr/objx v0.2.0 // indirect + github.com/stretchr/testify v1.6.1 + github.com/yuin/goldmark v1.1.32 // indirect golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20200602114024-627f9648deb9 - golang.org/x/sys v0.0.0-20200610111108-226ff32320da - golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae // indirect + golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a // indirect + golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 + golang.org/x/text v0.3.3 // indirect + golang.org/x/tools v0.0.0-20200618155944-c7475b9d7fb2 // indirect google.golang.org/appengine v1.6.1 // indirect gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2 // indirect - gopkg.in/yaml.v2 v2.3.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect ) diff --git a/go.sum b/go.sum index a2e2bb9236..869b3333f4 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 h1:IiCuOE1YCR github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759/go.mod h1:Kve3O9VpxZIHsPzpfxNdyFltFU9jBTeVYMYxSC99tdg= github.com/algorand/msgp v1.1.45 h1:uLEPvg0BfTrb2JjBcXexON8il4vv0EsD7HYFaA7e5jg= github.com/algorand/msgp v1.1.45/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= -github.com/algorand/oapi-codegen v1.3.5-algorand4 h1:skjv/lhlWAGnQPHQ9qdzTE5Bk9W555EKrh1bSul0JJs= -github.com/algorand/oapi-codegen v1.3.5-algorand4/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= github.com/algorand/oapi-codegen v1.3.5-algorand5 h1:y576Ca2/guQddQrQA7dtL5KcOx5xQgPeIupiuFMGyCI= github.com/algorand/oapi-codegen v1.3.5-algorand5/go.mod h1:/k0Ywn0lnt92uBMyE+yiRf/Wo3/chxHHsAfenD09EbY= github.com/algorand/websocket v1.4.1 h1:FPoNHI8i2VZWZzhCscY8JTzsAE7Vv73753cMbzb3udk= @@ -16,7 +14,9 @@ github.com/aws/aws-sdk-go v1.16.5 h1:NVxzZXIuwX828VcJrpNxxWjur1tlOBISdMdDdHIKHcc github.com/aws/aws-sdk-go v1.16.5/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/cpuguy83/go-md2man v1.0.8 h1:DwoNytLphI8hzS2Af4D0dfaEaiSq2bN05mEm4R6vf8M= github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= -github.com/cyberdelia/templates v0.0.0-20191230040416-20a325f050d4 h1:Fphwr1XDjkTR/KFbrrkLfY6D2CEOlHqFGomQQrxcHFs= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyberdelia/templates v0.0.0-20191230040416-20a325f050d4/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -34,13 +34,12 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/gen2brain/beeep v0.0.0-20180718162406-4e430518395f h1:eyHMPp7tXlBMF8PZHdsL89G0ehuRNflu7zKUeoQjcJ0= github.com/gen2brain/beeep v0.0.0-20180718162406-4e430518395f/go.mod h1:GprdPCZglWh5OMcIDpeKBxuUJI+fEDOTVUfxZeda4zo= github.com/getkin/kin-openapi v0.3.1/go.mod h1:W8dhxZgpE84ciM+VIItFqkmZ4eHtuomrdIHtASQIqi0= -github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE= -github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= github.com/getkin/kin-openapi v0.9.0 h1:/vaUQkiOR+vfFO3oilZentZTfAhz7OzXPhLdNas4q4w= github.com/getkin/kin-openapi v0.9.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw= +github.com/getkin/kin-openapi v0.14.0 h1:hqwQL7kze/adt0wB+0UJR2nJm+gfUHqM0Gu4D8nByVc= +github.com/getkin/kin-openapi v0.14.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi v4.1.1+incompatible h1:MmTgB0R8Bt/jccxp+t6S/1VGIKdJw5J74CK/c9tTfA4= github.com/go-chi/chi v4.1.1+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= @@ -72,8 +71,11 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2 h1:BkkpZxPVs3gIf+3Tejt8lWzuo2P29N1ChGUMEpuSJ8U= github.com/karalabe/hid v0.0.0-20181128192157-d815e0c1a2e2/go.mod h1:YvbcH+3Wo6XPs9nkgTY3u19KXLauXW+J5nB7hEHuX0A= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= @@ -81,9 +83,7 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54 h1:p8zN0Xu28xyEkPpqLbFXAnjdgBVvTJCpfOtoDf/+/RQ= github.com/matryer/moq v0.0.0-20200310130814-7721994d1b54/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= -github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6 h1:Cx1ZvZ3SQTli1nKee9qvJ/NJP3vt11s+ilM7NF3QSL8= github.com/matryer/moq v0.0.0-20200607124540-4638a53893e6/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= @@ -121,10 +121,14 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31 h1:OXcKh35JaYsGMRzpvFkLv/MEyPuL49CThT1pZ8aSml4= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -132,19 +136,15 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= -golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= -golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -156,17 +156,12 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= -golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -178,30 +173,25 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200430082407-1f5687305801 h1:Jp2/1+ZY++XrlALjnberpN8QkAUPNLkIjQIMInPpQxc= -golang.org/x/sys v0.0.0-20200430082407-1f5687305801/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w= -golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200423205358-59e73619c742 h1:9OGWpORUXvk8AsaBJlpzzDx7Srv/rSK6rvjcsJq4rJo= golang.org/x/tools v0.0.0-20200423205358-59e73619c742/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8 h1:BMFHd4OFnFtWX46Xj4DN6vvT1btiBxyq+s0orYBqcQY= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200518195306-39aadb5b76f2 h1:x5byaLtwftlfxkjQwPYSwshF9F3HE8iVApWvVu60NtY= -golang.org/x/tools v0.0.0-20200518195306-39aadb5b76f2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae h1:Tzc/wv256GkBNOkrWbHBeV3T08mrVevgo/q5KSGltr0= golang.org/x/tools v0.0.0-20200610052024-8d7dbee4c8ae/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2 h1:FD4wDsP+CQUqh2V12OBOt90pLHVToe58P++fUu3ggV4= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618155944-c7475b9d7fb2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -210,6 +200,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/sohlich/elogrus.v3 v3.0.0-20180410122755-1fa29e2f2009 h1:q/fZgS8MMadqFFGa8WL4Oyz+TmjiZfi8UrzWhTl8d5w= @@ -218,7 +209,9 @@ gopkg.in/toast.v1 v1.0.0-20180812000517-0a84660828b2/go.mod h1:s1Sn2yZos05Qfs7NK gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 0c1008c048..b21d1e3b73 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -32,9 +32,9 @@ import ( // accountsDbQueries is used to cache a prepared SQL statement to look up // the state of a single account. type accountsDbQueries struct { - listAssetsStmt *sql.Stmt + listCreatablesStmt *sql.Stmt lookupStmt *sql.Stmt - lookupAssetCreatorStmt *sql.Stmt + lookupCreatorStmt *sql.Stmt deleteStoredCatchpoint *sql.Stmt insertStoredCatchpoint *sql.Stmt selectOldestsCatchpointFiles *sql.Stmt @@ -79,6 +79,12 @@ var accountsSchema = []string{ strval text)`, } +// TODO: Post applications, rename assetcreators -> creatables and rename +// 'asset' column -> 'creatable' +var creatablesMigration = []string{ + `ALTER TABLE assetcreators ADD COLUMN ctype INTEGER DEFAULT 0`, +} + var accountsResetExprs = []string{ `DROP TABLE IF EXISTS acctrounds`, `DROP TABLE IF EXISTS accounttotals`, @@ -115,8 +121,8 @@ const ( catchpointStateCatchupBalancesRound = catchpointState("catchpointCatchupBalancesRound") ) -func writeCatchpointStagingAssets(ctx context.Context, tx *sql.Tx, addr basics.Address, assetIdx basics.AssetIndex) error { - _, err := tx.ExecContext(ctx, "INSERT INTO catchpointassetcreators(asset, creator) VALUES(?, ?)", assetIdx, addr[:]) +func writeCatchpointStagingCreatable(ctx context.Context, tx *sql.Tx, addr basics.Address, cidx basics.CreatableIndex, ctype basics.CreatableType) error { + _, err := tx.ExecContext(ctx, "INSERT INTO catchpointassetcreators(asset, creator, ctype) VALUES(?, ?, ?)", cidx, addr[:], ctype) if err != nil { return err } @@ -152,7 +158,7 @@ func resetCatchpointStagingBalances(ctx context.Context, tx *sql.Tx, newCatchup s += "DROP TABLE IF EXISTS catchpointaccounthashes;" s += "DELETE FROM accounttotals where id='catchpointStaging';" if newCatchup { - s += "CREATE TABLE IF NOT EXISTS catchpointassetcreators(asset integer primary key, creator blob);" + s += "CREATE TABLE IF NOT EXISTS catchpointassetcreators(asset integer primary key, creator blob, ctype integer);" s += "CREATE TABLE IF NOT EXISTS catchpointbalances(address blob primary key, data blob);" s += "CREATE TABLE IF NOT EXISTS catchpointaccounthashes(id integer primary key, data blob);" } @@ -172,6 +178,7 @@ func applyCatchpointStagingBalances(ctx context.Context, tx *sql.Tx, balancesRou s += "DROP TABLE IF EXISTS accountbase_old;" s += "DROP TABLE IF EXISTS assetcreators_old;" s += "DROP TABLE IF EXISTS accounthashes_old;" + _, err = tx.Exec(s) if err != nil { return err @@ -205,7 +212,22 @@ func accountsInit(tx *sql.Tx, initAccounts map[basics.Address]basics.AccountData } } - _, err := tx.Exec("INSERT INTO acctrounds (id, rnd) VALUES ('acctbase', 0)") + // Run creatables migration if it hasn't run yet + var creatableMigrated bool + err := tx.QueryRow("SELECT 1 FROM pragma_table_info('assetcreators') WHERE name='ctype'").Scan(&creatableMigrated) + if err == sql.ErrNoRows { + // Run migration + for _, migrateCmd := range creatablesMigration { + _, err = tx.Exec(migrateCmd) + if err != nil { + return err + } + } + } else if err != nil { + return err + } + + _, err = tx.Exec("INSERT INTO acctrounds (id, rnd) VALUES ('acctbase', 0)") if err == nil { var ot basics.OverflowTracker var totals AccountTotals @@ -275,7 +297,7 @@ func accountsDbInit(r db.Queryable, w db.Queryable) (*accountsDbQueries, error) var err error qs := &accountsDbQueries{} - qs.listAssetsStmt, err = r.Prepare("SELECT asset, creator FROM assetcreators WHERE asset <= ? ORDER BY asset desc LIMIT ?") + qs.listCreatablesStmt, err = r.Prepare("SELECT asset, creator, ctype FROM assetcreators WHERE asset <= ? AND ctype = ? ORDER BY asset desc LIMIT ?") if err != nil { return nil, err } @@ -285,7 +307,7 @@ func accountsDbInit(r db.Queryable, w db.Queryable) (*accountsDbQueries, error) return nil, err } - qs.lookupAssetCreatorStmt, err = r.Prepare("SELECT creator FROM assetcreators WHERE asset=?") + qs.lookupCreatorStmt, err = r.Prepare("SELECT creator FROM assetcreators WHERE asset = ? AND ctype = ?") if err != nil { return nil, err } @@ -332,10 +354,10 @@ func accountsDbInit(r db.Queryable, w db.Queryable) (*accountsDbQueries, error) return qs, nil } -func (qs *accountsDbQueries) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { +func (qs *accountsDbQueries) listCreatables(maxIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) (results []basics.CreatableLocator, err error) { err = db.Retry(func() error { // Query for assets in range - rows, err := qs.listAssetsStmt.Query(maxAssetIdx, maxResults) + rows, err := qs.listCreatablesStmt.Query(maxIdx, ctype, maxResults) if err != nil { return err } @@ -343,32 +365,37 @@ func (qs *accountsDbQueries) listAssets(maxAssetIdx basics.AssetIndex, maxResult // For each row, copy into a new CreatableLocator and append to results var buf []byte - var al basics.CreatableLocator + var cl basics.CreatableLocator for rows.Next() { - err := rows.Scan(&al.Index, &buf) + err := rows.Scan(&cl.Index, &buf) if err != nil { return err } - copy(al.Creator[:], buf) - results = append(results, al) + copy(cl.Creator[:], buf) + cl.Type = ctype + results = append(results, cl) } return nil }) return } -func (qs *accountsDbQueries) lookupAssetCreator(assetIdx basics.AssetIndex) (addr basics.Address, err error) { +func (qs *accountsDbQueries) lookupCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (addr basics.Address, ok bool, err error) { err = db.Retry(func() error { var buf []byte - err := qs.lookupAssetCreatorStmt.QueryRow(assetIdx).Scan(&buf) + err := qs.lookupCreatorStmt.QueryRow(cidx, ctype).Scan(&buf) + // Common error: creatable does not exist if err == sql.ErrNoRows { - err = fmt.Errorf("asset %d does not exist or has been deleted", assetIdx) + return nil } + // Some other database error if err != nil { return err } + + ok = true copy(addr[:], buf) return nil }) @@ -561,37 +588,7 @@ func accountsPutTotals(tx *sql.Tx, totals AccountTotals, catchpointStaging bool) return err } -// getChangedAssetIndices takes an accountDelta and returns which AssetIndices -// were created and which were deleted -func getChangedAssetIndices(creator basics.Address, delta accountDelta) map[basics.AssetIndex]modifiedAsset { - assetMods := make(map[basics.AssetIndex]modifiedAsset) - - // Get assets that were created - for idx := range delta.new.AssetParams { - // AssetParams are in now the balance record now, but _weren't_ before - if _, ok := delta.old.AssetParams[idx]; !ok { - assetMods[idx] = modifiedAsset{ - created: true, - creator: creator, - } - } - } - - // Get assets that were deleted - for idx := range delta.old.AssetParams { - // AssetParams were in the balance record, but _aren't_ anymore - if _, ok := delta.new.AssetParams[idx]; !ok { - assetMods[idx] = modifiedAsset{ - created: false, - creator: creator, - } - } - } - - return assetMods -} - -func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, rewardsLevel uint64, proto config.ConsensusParams) (err error) { +func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creatables map[basics.CreatableIndex]modifiedCreatable, rewardsLevel uint64, proto config.ConsensusParams) (err error) { var ot basics.OverflowTracker totals, err := accountsTotals(tx, false) if err != nil { @@ -612,17 +609,17 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, rewar } defer replaceStmt.Close() - insertAssetIdxStmt, err := tx.Prepare("INSERT INTO assetcreators (asset, creator) VALUES (?, ?)") + insertCreatableIdxStmt, err := tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") if err != nil { return } - defer insertAssetIdxStmt.Close() + defer insertCreatableIdxStmt.Close() - deleteAssetIdxStmt, err := tx.Prepare("DELETE FROM assetcreators WHERE asset=?") + deleteCreatableIdxStmt, err := tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") if err != nil { return } - defer deleteAssetIdxStmt.Close() + defer deleteCreatableIdxStmt.Close() for addr, data := range updates { if data.new.IsZero() { @@ -637,17 +634,16 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, rewar totals.delAccount(proto, data.old, &ot) totals.addAccount(proto, data.new, &ot) + } - adeltas := getChangedAssetIndices(addr, data) - for aidx, delta := range adeltas { - if delta.created { - _, err = insertAssetIdxStmt.Exec(aidx, addr[:]) - } else { - _, err = deleteAssetIdxStmt.Exec(aidx) - } - if err != nil { - return - } + for cidx, cdelta := range creatables { + if cdelta.created { + _, err = insertCreatableIdxStmt.Exec(cidx, cdelta.creator[:], cdelta.ctype) + } else { + _, err = deleteCreatableIdxStmt.Exec(cidx, cdelta.ctype) + } + if err != nil { + return } } diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index 0838456e30..44020b6cba 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -203,7 +203,7 @@ func TestAccountDBRound(t *testing.T) { for i := 1; i < 10; i++ { updates, newaccts, _ := randomDeltas(20, accts, 0) accts = newaccts - err = accountsNewRound(tx, updates, 0, proto) + err = accountsNewRound(tx, updates, nil, 0, proto) require.NoError(t, err) err = updateAccountsRound(tx, basics.Round(i), 0) require.NoError(t, err) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 0a8aca97f9..be55302d96 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -69,15 +69,18 @@ type modifiedAccount struct { ndeltas int } -type modifiedAsset struct { +type modifiedCreatable struct { + // Type of the creatable: app or asset + ctype basics.CreatableType + // Created if true, deleted if false created bool - // Creator is the creator of the asset + // creator of the app/asset creator basics.Address - // Keeps track of how many times this asset appears in - // accountUpdates.assetDeltas + // Keeps track of how many times this app/asset appears in + // accountUpdates.creatableDeltas ndeltas int } @@ -122,12 +125,12 @@ type accountUpdates struct { // address that appears in deltas. accounts map[basics.Address]modifiedAccount - // assetDeltas stores asset updates for every round after dbRound. - assetDeltas []map[basics.AssetIndex]modifiedAsset + // creatableDeltas stores creatable updates for every round after dbRound. + creatableDeltas []map[basics.CreatableIndex]modifiedCreatable - // assets stores the most recent asset state for every asset - // that appears in assetDeltas - assets map[basics.AssetIndex]modifiedAsset + // creatables stores the most recent state for every creatable that + // appears in creatableDeltas + creatables map[basics.CreatableIndex]modifiedCreatable // protos stores consensus parameters dbRound and every // round after it; i.e., protos is one longer than deltas. @@ -292,51 +295,63 @@ func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewa } func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.CreatableLocator, error) { + return au.listCreatables(basics.CreatableIndex(maxAssetIdx), maxResults, basics.AssetCreatable) +} + +func (au *accountUpdates) listApplications(maxAppIdx basics.AppIndex, maxResults uint64) ([]basics.CreatableLocator, error) { + return au.listCreatables(basics.CreatableIndex(maxAppIdx), maxResults, basics.AppCreatable) +} + +func (au *accountUpdates) listCreatables(maxCreatableIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) ([]basics.CreatableLocator, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - // Sort indices for assets that have been created/deleted. If this + // Sort indices for creatables that have been created/deleted. If this // turns out to be too inefficient, we could keep around a heap of // created/deleted asset indices in memory. - keys := make([]basics.AssetIndex, 0, len(au.assets)) - for aidx := range au.assets { - if aidx <= maxAssetIdx { - keys = append(keys, aidx) + keys := make([]basics.CreatableIndex, 0, len(au.creatables)) + for cidx, delta := range au.creatables { + if delta.ctype != ctype { + continue + } + if cidx <= maxCreatableIdx { + keys = append(keys, cidx) } } sort.Slice(keys, func(i, j int) bool { return keys[i] > keys[j] }) - // Check for assets that haven't been synced to disk yet. - var unsyncedAssets []basics.CreatableLocator - deletedAssets := make(map[basics.AssetIndex]bool) - for _, aidx := range keys { - delta := au.assets[aidx] + // Check for creatables that haven't been synced to disk yet. + var unsyncedCreatables []basics.CreatableLocator + deletedCreatables := make(map[basics.CreatableIndex]bool) + for _, cidx := range keys { + delta := au.creatables[cidx] if delta.created { - // Created asset that only exists in memory - unsyncedAssets = append(unsyncedAssets, basics.CreatableLocator{ - Index: basics.CreatableIndex(aidx), + // Created but only exists in memory + unsyncedCreatables = append(unsyncedCreatables, basics.CreatableLocator{ + Type: delta.ctype, + Index: cidx, Creator: delta.creator, }) } else { - // Mark deleted assets for exclusion from the results set - deletedAssets[aidx] = true + // Mark deleted creatables for exclusion from the results set + deletedCreatables[cidx] = true } } - // Check in-memory created assets, which will always be newer than anything + // Check in-memory created creatables, which will always be newer than anything // in the database var res []basics.CreatableLocator - for _, loc := range unsyncedAssets { + for _, loc := range unsyncedCreatables { if uint64(len(res)) == maxResults { return res, nil } res = append(res, loc) } - // Fetch up to maxResults - len(res) + len(deletedAssets) from the database, so we - // have enough extras in case assets were deleted - numToFetch := maxResults - uint64(len(res)) + uint64(len(deletedAssets)) - dbResults, err := au.accountsq.listAssets(maxAssetIdx, numToFetch) + // Fetch up to maxResults - len(res) + len(deletedCreatables) from the database, + // so we have enough extras in case creatables were deleted + numToFetch := maxResults - uint64(len(res)) + uint64(len(deletedCreatables)) + dbResults, err := au.accountsq.listCreatables(maxCreatableIdx, numToFetch, ctype) if err != nil { return nil, err } @@ -348,8 +363,8 @@ func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults u return res, nil } - // Asset was deleted - if _, ok := deletedAssets[basics.AssetIndex(loc.Index)]; ok { + // Creatable was deleted + if _, ok := deletedCreatables[loc.Index]; ok { continue } @@ -367,41 +382,41 @@ func (au *accountUpdates) getLastCatchpointLabel() string { return au.lastCatchpointLabel } -// getAssetCreatorForRound returns the asset creator for a given asset index at a given round -func (au *accountUpdates) getAssetCreatorForRound(rnd basics.Round, aidx basics.AssetIndex) (basics.Address, error) { +func (au *accountUpdates) getCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() + offset, err := au.roundOffset(rnd) if err != nil { - return basics.Address{}, err + return basics.Address{}, false, err } - // If this is the most recent round, au.assets has will have the latest - // state and we can skip scanning backwards over assetDeltas + // If this is the most recent round, au.creatables has will have the latest + // state and we can skip scanning backwards over creatableDeltas if offset == uint64(len(au.deltas)) { // Check if we already have the asset/creator in cache - assetDelta, ok := au.assets[aidx] + creatableDelta, ok := au.creatables[cidx] if ok { - if assetDelta.created { - return assetDelta.creator, nil + if creatableDelta.created && creatableDelta.ctype == ctype { + return creatableDelta.creator, true, nil } - return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) + return basics.Address{}, false, nil } } else { for offset > 0 { offset-- - assetDelta, ok := au.assetDeltas[offset][aidx] + creatableDelta, ok := au.creatableDeltas[offset][cidx] if ok { - if assetDelta.created { - return assetDelta.creator, nil + if creatableDelta.created && creatableDelta.ctype == ctype { + return creatableDelta.creator, true, nil } - return basics.Address{}, fmt.Errorf("asset %v has been deleted", aidx) + return basics.Address{}, false, nil } } } // Check the database - return au.accountsq.lookupAssetCreator(aidx) + return au.accountsq.lookupCreator(cidx, ctype) } // committedUpTo enqueues commiting the balances for round committedRound-lookback. @@ -530,7 +545,7 @@ func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { } au.deltas = append(au.deltas, delta.accts) au.protos = append(au.protos, proto) - au.assetDeltas = append(au.assetDeltas, delta.assets) + au.creatableDeltas = append(au.creatableDeltas, delta.creatables) au.roundDigest = append(au.roundDigest, blk.Digest()) au.deltasAccum = append(au.deltasAccum, len(delta.accts)+au.deltasAccum[len(au.deltasAccum)-1]) @@ -549,12 +564,13 @@ func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { au.accounts[addr] = macct } - for aidx, adelta := range delta.assets { - masset := au.assets[aidx] - masset.creator = adelta.creator - masset.created = adelta.created - masset.ndeltas++ - au.assets[aidx] = masset + for cidx, cdelta := range delta.creatables { + mcreat := au.creatables[cidx] + mcreat.creator = cdelta.creator + mcreat.created = cdelta.created + mcreat.ctype = cdelta.ctype + mcreat.ndeltas++ + au.creatables[cidx] = mcreat } if ot.Overflowed { @@ -726,9 +742,9 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo } au.protos = []config.ConsensusParams{config.Consensus[hdr.CurrentProtocol]} au.deltas = nil - au.assetDeltas = nil + au.creatableDeltas = nil au.accounts = make(map[basics.Address]modifiedAccount) - au.assets = make(map[basics.AssetIndex]modifiedAsset) + au.creatables = make(map[basics.CreatableIndex]modifiedCreatable) au.deltasAccum = []int{0} // keep these channel closed if we're not generating catchpoint @@ -949,9 +965,11 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb // create a copy of the deltas, round totals and protos for the range we're going to flush. deltas := make([]map[basics.Address]accountDelta, offset, offset) + creatableDeltas := make([]map[basics.CreatableIndex]modifiedCreatable, offset, offset) roundTotals := make([]AccountTotals, offset+1, offset+1) protos := make([]config.ConsensusParams, offset+1, offset+1) copy(deltas, au.deltas[:offset]) + copy(creatableDeltas, au.creatableDeltas[:offset]) copy(roundTotals, au.roundTotals[:offset+1]) copy(protos, au.protos[:offset+1]) @@ -959,12 +977,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb // account DB, so that we can drop the corresponding refcounts in // au.accounts. flushcount := make(map[basics.Address]int) - assetFlushcount := make(map[basics.AssetIndex]int) - for i := uint64(0); i < offset; i++ { - for aidx := range au.assetDeltas[i] { - assetFlushcount[aidx] = assetFlushcount[aidx] + 1 - } - } + creatableFlushcount := make(map[basics.CreatableIndex]int) var committedRoundDigest crypto.Digest @@ -987,6 +1000,9 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb for addr := range deltas[i] { flushcount[addr] = flushcount[addr] + 1 } + for cidx := range creatableDeltas[i] { + creatableFlushcount[cidx] = creatableFlushcount[cidx] + 1 + } } var catchpointLabel string @@ -1012,7 +1028,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { - err = accountsNewRound(tx, deltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) + err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) if err != nil { return err } @@ -1076,21 +1092,21 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } } - for aidx, cnt := range assetFlushcount { - masset, ok := au.assets[aidx] + for cidx, cnt := range creatableFlushcount { + mcreat, ok := au.creatables[cidx] if !ok { - au.log.Panicf("inconsistency: flushed %d changes to asset %d, but not in au.assets", cnt, aidx) + au.log.Panicf("inconsistency: flushed %d changes to creatable %d, but not in au.creatables", cnt, cidx) } - if cnt > masset.ndeltas { - au.log.Panicf("inconsistency: flushed %d changes to asset %d, but au.assets had %d", cnt, aidx, masset.ndeltas) + if cnt > mcreat.ndeltas { + au.log.Panicf("inconsistency: flushed %d changes to creatable %d, but au.creatables had %d", cnt, cidx, mcreat.ndeltas) } - masset.ndeltas -= cnt - if masset.ndeltas == 0 { - delete(au.assets, aidx) + mcreat.ndeltas -= cnt + if mcreat.ndeltas == 0 { + delete(au.creatables, cidx) } else { - au.assets[aidx] = masset + au.creatables[cidx] = mcreat } } @@ -1099,7 +1115,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb au.roundDigest = au.roundDigest[offset:] au.protos = au.protos[offset:] au.roundTotals = au.roundTotals[offset:] - au.assetDeltas = au.assetDeltas[offset:] + au.creatableDeltas = au.creatableDeltas[offset:] au.dbRound = newBase au.lastFlushTime = flushTime diff --git a/ledger/applications.go b/ledger/applications.go new file mode 100644 index 0000000000..6ee9e67aa7 --- /dev/null +++ b/ledger/applications.go @@ -0,0 +1,238 @@ +// 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 . + +package ledger + +import ( + "fmt" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" +) + +// AppTealGlobals contains data accessible by the "global" opcode. +type AppTealGlobals struct { + CurrentRound basics.Round + LatestTimestamp int64 +} + +// appTealEvaluator implements transactions.StateEvaluator. When applying an +// ApplicationCall transaction, InitLedger is called, followed by Check and/or +// Eval. These pass the initialized LedgerForLogic (appLedger) to the TEAL +// interpreter. +type appTealEvaluator struct { + evalParams logic.EvalParams + AppTealGlobals +} + +// appLedger implements logic.LedgerForLogic +type appLedger struct { + balances transactions.Balances + appIdx basics.AppIndex + schemas basics.StateSchemas + AppTealGlobals +} + +// Eval evaluates a stateful TEAL program for an application. InitLedger must +// be called before calling Eval. +func (ae *appTealEvaluator) Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) { + if ae.evalParams.Ledger == nil { + err = fmt.Errorf("appTealEvaluator Ledger not initialized") + return + } + return logic.EvalStateful(program, ae.evalParams) +} + +// Check computes the cost of a TEAL program for an application. InitLedger must +// be called before calling Check. +func (ae *appTealEvaluator) Check(program []byte) (cost int, err error) { + if ae.evalParams.Ledger == nil { + err = fmt.Errorf("appTealEvaluator Ledger not initialized") + return + } + return logic.CheckStateful(program, ae.evalParams) +} + +// InitLedger initializes an appLedger, which satisfies the +// logic.LedgerForLogic interface. +func (ae *appTealEvaluator) InitLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { + ledger, err := newAppLedger(balances, appIdx, schemas, ae.AppTealGlobals) + if err != nil { + return err + } + + ae.evalParams.Ledger = ledger + return nil +} + +func newAppLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (al *appLedger, err error) { + if balances == nil { + err = fmt.Errorf("cannot create appLedger with nil balances") + return + } + + if appIdx == 0 { + err = fmt.Errorf("cannot create appLedger for appIdx 0") + return + } + + al = &appLedger{} + al.appIdx = appIdx + al.balances = balances + al.schemas = schemas + al.AppTealGlobals = globals + return al, nil +} + +// MakeDebugAppLedger returns logic.LedgerForLogic suitable for debug or dryrun +func MakeDebugAppLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (logic.LedgerForLogic, error) { + return newAppLedger(balances, appIdx, schemas, globals) +} + +func (al *appLedger) Balance(addr basics.Address) (res basics.MicroAlgos, err error) { + // Fetch record with pending rewards applied + record, err := al.balances.Get(addr, true) + if err != nil { + return + } + + return record.MicroAlgos, nil +} + +// AppGlobalState returns the global state key/value store for the requested +// application. The returned map must NOT be modified. +func (al *appLedger) AppGlobalState(appIdx basics.AppIndex) (basics.TealKeyValue, error) { + // Allow referring to the current appIdx as 0 + var params basics.AppParams + if appIdx == 0 { + appIdx = al.appIdx + } + + // Find app creator (and check if app exists) + creator, ok, err := al.balances.GetCreator(basics.CreatableIndex(appIdx), basics.AppCreatable) + if err != nil { + return nil, err + } + + // Ensure app exists + if !ok { + return nil, fmt.Errorf("app %d does not exist", appIdx) + } + + // Fetch creator's balance record + record, err := al.balances.Get(creator, false) + if err != nil { + return nil, err + } + + // Ensure creator has expected params + params, ok = record.AppParams[appIdx] + if !ok { + return nil, fmt.Errorf("app %d not found in account %s", appIdx, creator.String()) + } + + // GlobalState might be nil, so make sure we don't return a nil map + keyValue := params.GlobalState + if keyValue == nil { + keyValue = make(basics.TealKeyValue) + } + return keyValue, nil +} + +// AppLocalState returns the local state key/value store for this +// account and application. The returned map must NOT be modified. +func (al *appLedger) AppLocalState(addr basics.Address, appIdx basics.AppIndex) (basics.TealKeyValue, error) { + // Allow referring to the current appIdx as 0 + if appIdx == 0 { + appIdx = al.appIdx + } + + // Don't fetch with pending rewards here since we are only returning + // the LocalState, not the balance + record, err := al.balances.Get(addr, false) + if err != nil { + return nil, err + } + + // Ensure account is opted in + localState, ok := record.AppLocalStates[appIdx] + if !ok { + return nil, fmt.Errorf("addr %s not opted in to app %d, cannot fetch state", addr.String(), appIdx) + } + + // KeyValue might be nil, so make sure we don't return a nil map + keyValue := localState.KeyValue + if keyValue == nil { + keyValue = make(basics.TealKeyValue) + } + + return keyValue, nil +} + +func (al *appLedger) AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) { + // Fetch the requested balance record + record, err := al.balances.Get(addr, false) + if err != nil { + return basics.AssetHolding{}, err + } + + // Ensure we have the requested holding + holding, ok := record.Assets[assetIdx] + if !ok { + err = fmt.Errorf("account %s has not opted in to asset %d", addr.String(), assetIdx) + return basics.AssetHolding{}, err + } + + return holding, nil +} + +func (al *appLedger) AssetParams(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetParams, error) { + // Fetch the requested balance record + record, err := al.balances.Get(addr, false) + if err != nil { + return basics.AssetParams{}, err + } + + // Ensure account created the requested asset + params, ok := record.AssetParams[assetIdx] + if !ok { + err = fmt.Errorf("account %s has not created asset %d", addr.String(), assetIdx) + return basics.AssetParams{}, err + } + + return params, nil +} + +func (al *appLedger) Round() basics.Round { + return al.AppTealGlobals.CurrentRound +} + +func (al *appLedger) LatestTimestamp() int64 { + return al.AppTealGlobals.LatestTimestamp +} + +func (al *appLedger) ApplicationID() basics.AppIndex { + return al.appIdx +} + +func (al *appLedger) LocalSchema() basics.StateSchema { + return al.schemas.LocalStateSchema +} + +func (al *appLedger) GlobalSchema() basics.StateSchema { + return al.schemas.GlobalStateSchema +} diff --git a/ledger/applications_test.go b/ledger/applications_test.go new file mode 100644 index 0000000000..0ba423b7f0 --- /dev/null +++ b/ledger/applications_test.go @@ -0,0 +1,271 @@ +// 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 . + +package ledger + +import ( + "crypto/rand" + "fmt" + "testing" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/stretchr/testify/require" +) + +type testBalances struct { + appCreators map[basics.AppIndex]basics.Address + balances map[basics.Address]basics.AccountData +} + +type testBalancesPass struct { + testBalances +} + +const appIdxError basics.AppIndex = 0x11223344 +const appIdxOk basics.AppIndex = 1 + +func (b *testBalances) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalances) Put(record basics.BalanceRecord) error { + return nil +} + +func (b *testBalances) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { + return nil +} + +func (b *testBalances) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + if ctype == basics.AppCreatable { + aidx := basics.AppIndex(cidx) + if aidx == appIdxError { // magic for test + return basics.Address{}, false, fmt.Errorf("mock synthetic error") + } + + creator, ok := b.appCreators[aidx] + return creator, ok, nil + } + return basics.Address{}, false, nil +} + +func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { + return nil +} + +func (b *testBalancesPass) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalancesPass) Put(record basics.BalanceRecord) error { + return nil +} + +func (b *testBalancesPass) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { + return nil +} + +func (b *testBalances) ConsensusParams() config.ConsensusParams { + return config.ConsensusParams{} +} + +func getRandomAddress(a *require.Assertions) basics.Address { + const rl = 16 + b := make([]byte, rl) + n, err := rand.Read(b) + a.NoError(err) + a.Equal(rl, n) + + address := crypto.Hash(b) + return basics.Address(address) +} + +func TestNewAppLedger(t *testing.T) { + a := require.New(t) + + _, err := newAppLedger(nil, 0, basics.StateSchemas{}, AppTealGlobals{}) + a.Error(err) + a.Contains(err.Error(), "nil balances") + + b := testBalances{} + + _, err = newAppLedger(&b, 0, basics.StateSchemas{}, AppTealGlobals{}) + a.Error(err) + a.Contains(err.Error(), "cannot create appLedger for appIdx 0") + + appIdx := basics.AppIndex(1) + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(l) + a.Equal(appIdx, l.appIdx) + a.NotNil(l.balances) + + dl, err := MakeDebugAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(dl) +} + +func TestAppLedgerBalances(t *testing.T) { + a := require.New(t) + + b := testBalances{} + addr1 := getRandomAddress(a) + appIdx := appIdxOk + + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(l) + + ble := basics.MicroAlgos{Raw: 100} + b.balances = map[basics.Address]basics.AccountData{addr1: {MicroAlgos: ble}} + bla, err := l.Balance(addr1) + a.NoError(err) + a.Equal(ble, bla) +} + +func TestAppLedgerGetters(t *testing.T) { + a := require.New(t) + + b := testBalances{} + appIdx := appIdxOk + round := basics.Round(1234) + ts := int64(11223344) + globals := AppTealGlobals{round, ts} + + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, globals) + a.NoError(err) + a.NotNil(l) + + a.Equal(appIdx, l.ApplicationID()) + a.Equal(round, l.Round()) + a.Equal(ts, l.LatestTimestamp()) + + // check there no references/pointers + globals.CurrentRound = 101 + globals.LatestTimestamp = 102 + a.Equal(round, l.Round()) + a.Equal(ts, l.LatestTimestamp()) +} + +func TestAppLedgerAsset(t *testing.T) { + a := require.New(t) + + b := testBalances{} + addr1 := getRandomAddress(a) + appIdx := appIdxOk + + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(l) + + assetIdx := basics.AssetIndex(2) + b.balances = map[basics.Address]basics.AccountData{addr1: {}} + _, err = l.AssetParams(addr1, assetIdx) + a.Error(err) + a.Contains(err.Error(), "has not created asset") + + b.balances[addr1] = basics.AccountData{ + AssetParams: map[basics.AssetIndex]basics.AssetParams{assetIdx: {Total: 1000}}, + } + ap, err := l.AssetParams(addr1, assetIdx) + a.NoError(err) + a.Equal(uint64(1000), ap.Total) + delete(b.balances, addr1) + + b.balances = map[basics.Address]basics.AccountData{addr1: {}} + _, err = l.AssetHolding(addr1, assetIdx) + a.Error(err) + a.Contains(err.Error(), "has not opted in to asset") + + b.balances[addr1] = basics.AccountData{ + Assets: map[basics.AssetIndex]basics.AssetHolding{assetIdx: {Amount: 99}}, + } + ah, err := l.AssetHolding(addr1, assetIdx) + a.NoError(err) + a.Equal(uint64(99), ah.Amount) +} + +func TestAppLedgerAppGlobalState(t *testing.T) { + a := require.New(t) + + b := testBalances{} + addr1 := getRandomAddress(a) + appIdx := appIdxOk + + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(l) + + _, err = l.AppGlobalState(appIdx) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("app %d does not exist", appIdx)) + + _, err = l.AppGlobalState(0) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("app %d does not exist", appIdx)) + + _, err = l.AppGlobalState(appIdxError) + a.Error(err) + a.NotContains(err.Error(), "cannot access global state") + a.NotContains(err.Error(), fmt.Sprintf("app %d does not exist", appIdxError)) + + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: addr1} + _, err = l.AppGlobalState(appIdx) + a.Error(err) + a.NotContains(err.Error(), fmt.Sprintf("app %d does not exist", appIdx)) + + b.balances = map[basics.Address]basics.AccountData{addr1: {}} + _, err = l.AppGlobalState(appIdx) + a.Error(err) + a.Contains(err.Error(), "not found in account") + + b.balances[addr1] = basics.AccountData{AppParams: map[basics.AppIndex]basics.AppParams{appIdx: {}}} + kv, err := l.AppGlobalState(appIdx) + a.NoError(err) + a.NotNil(kv) +} + +func TestAppLedgerAppLocalState(t *testing.T) { + a := require.New(t) + + b := testBalances{} + addr1 := getRandomAddress(a) + appIdx := basics.AppIndex(100) // not in app + + l, err := newAppLedger(&b, appIdx, basics.StateSchemas{}, AppTealGlobals{}) + a.NoError(err) + a.NotNil(l) + + b.balances = map[basics.Address]basics.AccountData{addr1: {}} + _, err = l.AppLocalState(addr1, appIdx) + a.Error(err) + a.Contains(err.Error(), "not opted in to app") + + b.balances[addr1] = basics.AccountData{AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}} + kv, err := l.AppLocalState(addr1, appIdx) + a.NoError(err) + a.NotNil(kv) +} diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 4bcf87116a..0890ed0d78 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -21,6 +21,7 @@ import ( "database/sql" "fmt" "io/ioutil" + mathrand "math/rand" "os" "path/filepath" "reflect" @@ -34,7 +35,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/libgoal" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" ) @@ -243,19 +244,58 @@ func makeUnsignedAssetCreateTx(firstValid, lastValid basics.Round, total uint64, return tx, nil } -func makeUnsignedAssetDestroyTx(client libgoal.Client, firstValid, lastValid basics.Round, assetIndex uint64) (transactions.Transaction, error) { - txn, err := client.MakeUnsignedAssetDestroyTx(assetIndex) - if err != nil { - return transactions.Transaction{}, err - } +func makeUnsignedAssetDestroyTx(firstValid, lastValid basics.Round, assetIndex uint64) (transactions.Transaction, error) { + var txn transactions.Transaction + txn.Type = protocol.AssetConfigTx + txn.ConfigAsset = basics.AssetIndex(assetIndex) txn.FirstValid = firstValid txn.LastValid = lastValid return txn, nil } -func TestArchivalAssets(t *testing.T) { - // Start in archival mode, add 2K blocks with asset txns, restart, ensure all - // assets are there in index unless they were deleted +func makeUnsignedApplicationCallTx(appIdx uint64, onCompletion transactions.OnCompletion) (tx transactions.Transaction, err error) { + tx.Type = protocol.ApplicationCallTx + tx.ApplicationID = basics.AppIndex(appIdx) + tx.OnCompletion = onCompletion + + // If creating, set programs + if appIdx == 0 { + testprog := `#pragma version 2 + // Write global["foo"] = "bar" + byte base64 Zm9v + byte base64 YmFy + app_global_put + + // Write sender.local["foo"] = "bar" + // txn.Sender + int 0 + byte base64 Zm9v + byte base64 YmFy + app_local_put + + int 1 + ` + prog, err := logic.AssembleString(testprog) + if err != nil { + return tx, err + } + tx.ApprovalProgram = prog + tx.ClearStateProgram = prog + tx.GlobalStateSchema = basics.StateSchema{ + NumByteSlice: 1, + } + tx.LocalStateSchema = basics.StateSchema{ + NumByteSlice: 1, + } + } + + return tx, nil +} + +func TestArchivalCreatables(t *testing.T) { + // Start in archival mode, add 2K blocks with asset + app txns + // restart, ensure all assets are there in index unless they were + // deleted dbTempDir, err := ioutil.TempDir("", "testdir"+t.Name()) require.NoError(t, err) @@ -271,9 +311,14 @@ func TestArchivalAssets(t *testing.T) { genesisInitState.GenesisHash = crypto.Digest{1} genesisInitState.Block.BlockHeader.GenesisHash = crypto.Digest{1} - // true = created, false = deleted - assetIdxs := make(map[basics.AssetIndex]bool) - assetCreators := make(map[basics.AssetIndex]basics.Address) + type ActionEnum uint64 + const AssetCreated ActionEnum = 0 + const AppCreated ActionEnum = 1 + const AssetDeleted ActionEnum = 2 + const AppDeleted ActionEnum = 3 + + creatableIdxs := make(map[basics.CreatableIndex]ActionEnum) + allCreators := make(map[basics.CreatableIndex]basics.Address) // keep track of existing/deleted assets for sanity check at end var expectedExisting int @@ -298,23 +343,33 @@ func TestArchivalAssets(t *testing.T) { require.NoError(t, err) blk := genesisInitState.Block - // create assets - client := libgoal.Client{} + // keep track of max created idx + var maxCreated uint64 + + // create apps and assets for i := 0; i < maxBlocks; i++ { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) - // make a transaction that will create an asset + // make a transaction that will create an asset or application creatorEncoded := creators[i].String() - tx, err := makeUnsignedAssetCreateTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, 100, false, creatorEncoded, creatorEncoded, creatorEncoded, creatorEncoded, "m", "m", "", nil) + createdIdx := basics.CreatableIndex(blk.BlockHeader.TxnCounter + 1) + var tx transactions.Transaction + if mathrand.Float32() < 0.5 { + creatableIdxs[createdIdx] = AssetCreated + tx, err = makeUnsignedAssetCreateTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, 100, false, creatorEncoded, creatorEncoded, creatorEncoded, creatorEncoded, "m", "m", "", nil) + } else { + creatableIdxs[createdIdx] = AppCreated + tx, err = makeUnsignedApplicationCallTx(0, transactions.OptInOC) + } require.NoError(t, err) + + maxCreated = uint64(createdIdx) + tx.Sender = creators[i] - createdAssetIdx := basics.AssetIndex(blk.BlockHeader.TxnCounter + 1) expectedExisting++ - // mark asset as created for checking later - assetIdxs[createdAssetIdx] = true - assetCreators[createdAssetIdx] = tx.Sender + allCreators[createdIdx] = tx.Sender blk.BlockHeader.TxnCounter++ // make a payset @@ -323,13 +378,21 @@ func TestArchivalAssets(t *testing.T) { payset = append(payset, stxnib) blk.Payset = payset - // for one of the assets, delete it in the same block - if i == maxBlocks/2 { - tx0, err := makeUnsignedAssetDestroyTx(client, blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, blk.BlockHeader.TxnCounter) + // for some of the assets/apps, delete it in the same block + if i >= maxBlocks/2 && i < (3*(maxBlocks/4)) { + switch creatableIdxs[createdIdx] { + case AssetCreated: + tx, err = makeUnsignedAssetDestroyTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, uint64(createdIdx)) + creatableIdxs[createdIdx] = AssetDeleted + case AppCreated: + tx, err = makeUnsignedApplicationCallTx(uint64(createdIdx), transactions.DeleteApplicationOC) + creatableIdxs[createdIdx] = AppDeleted + default: + panic("unknown action") + } require.NoError(t, err) - tx0.Sender = assetCreators[createdAssetIdx] - blk.Payset = append(blk.Payset, makeSignedTxnInBlock(tx0)) - assetIdxs[createdAssetIdx] = false + tx.Sender = allCreators[createdIdx] + blk.Payset = append(blk.Payset, makeSignedTxnInBlock(tx)) blk.BlockHeader.TxnCounter++ expectedExisting-- expectedDeleted++ @@ -341,24 +404,43 @@ func TestArchivalAssets(t *testing.T) { } l.WaitForCommit(blk.Round()) - // check that we can fetch creator for all created assets and can't for - // deleted assets + // check that we can fetch creator for all created assets/apps and + // can't for deleted assets/apps var existing, deleted int - for aidx, status := range assetIdxs { - c, err := l.GetAssetCreator(aidx) - if status { + for aidx, status := range creatableIdxs { + switch status { + case AssetCreated: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) require.NoError(t, err) - require.Equal(t, assetCreators[aidx], c) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) existing++ - } else { - require.Error(t, err) + case AppCreated: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) + existing++ + case AssetDeleted: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, basics.Address{}, c) + deleted++ + case AppDeleted: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.False(t, ok) require.Equal(t, basics.Address{}, c) deleted++ + default: + panic("unknown action") } } + require.Equal(t, expectedExisting, existing) require.Equal(t, expectedDeleted, deleted) - require.Equal(t, len(assetIdxs), existing+deleted) + require.Equal(t, len(creatableIdxs), existing+deleted) // close and reopen the same DB l.Close() @@ -370,37 +452,73 @@ func TestArchivalAssets(t *testing.T) { // deleted assets existing = 0 deleted = 0 - for aidx, status := range assetIdxs { - c, err := l.GetAssetCreator(aidx) - if status { + for aidx, status := range creatableIdxs { + switch status { + case AssetCreated: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) require.NoError(t, err) - require.Equal(t, assetCreators[aidx], c) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) existing++ - } else { - require.Error(t, err) + case AppCreated: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) + existing++ + case AssetDeleted: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) + require.NoError(t, err) + require.False(t, ok) require.Equal(t, basics.Address{}, c) deleted++ + case AppDeleted: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, basics.Address{}, c) + deleted++ + default: + panic("unknown action") } } require.Equal(t, expectedExisting, existing) require.Equal(t, expectedDeleted, deleted) - require.Equal(t, len(assetIdxs), existing+deleted) - - // delete an old asset and a new asset - assetToDelete := basics.AssetIndex(1) - tx0, err := makeUnsignedAssetDestroyTx(client, blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, uint64(assetToDelete)) + require.Equal(t, len(creatableIdxs), existing+deleted) + + // delete an old creatable and a new creatable + creatableToDelete := basics.CreatableIndex(1) + var tx0 transactions.Transaction + switch creatableIdxs[creatableToDelete] { + case AssetCreated: + tx0, err = makeUnsignedAssetDestroyTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, uint64(creatableToDelete)) + creatableIdxs[creatableToDelete] = AssetDeleted + case AppCreated: + tx0, err = makeUnsignedApplicationCallTx(uint64(creatableToDelete), transactions.DeleteApplicationOC) + creatableIdxs[creatableToDelete] = AppDeleted + default: + panic("unknown action") + } require.NoError(t, err) - tx0.Sender = assetCreators[assetToDelete] - assetIdxs[assetToDelete] = false + tx0.Sender = allCreators[creatableToDelete] blk.BlockHeader.TxnCounter++ expectedExisting-- expectedDeleted++ - assetToDelete = basics.AssetIndex(maxBlocks) - tx1, err := makeUnsignedAssetDestroyTx(client, blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, uint64(assetToDelete)) + creatableToDelete = basics.CreatableIndex(maxCreated) + var tx1 transactions.Transaction + switch creatableIdxs[creatableToDelete] { + case AssetCreated: + tx1, err = makeUnsignedAssetDestroyTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, uint64(creatableToDelete)) + creatableIdxs[creatableToDelete] = AssetDeleted + case AppCreated: + tx1, err = makeUnsignedApplicationCallTx(uint64(creatableToDelete), transactions.DeleteApplicationOC) + creatableIdxs[creatableToDelete] = AppDeleted + default: + panic("unknown action") + } require.NoError(t, err) - tx1.Sender = assetCreators[assetToDelete] - assetIdxs[assetToDelete] = false + tx1.Sender = allCreators[creatableToDelete] blk.BlockHeader.TxnCounter++ expectedExisting-- expectedDeleted++ @@ -424,21 +542,39 @@ func TestArchivalAssets(t *testing.T) { // deleted assets existing = 0 deleted = 0 - for aidx, status := range assetIdxs { - c, err := l.GetAssetCreator(aidx) - if status { + for aidx, status := range creatableIdxs { + switch status { + case AssetCreated: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) require.NoError(t, err) - require.Equal(t, assetCreators[aidx], c) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) existing++ - } else { - require.Error(t, err) + case AppCreated: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) + existing++ + case AssetDeleted: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) + require.NoError(t, err) + require.False(t, ok) require.Equal(t, basics.Address{}, c) deleted++ + case AppDeleted: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, basics.Address{}, c) + deleted++ + default: + panic("unknown action") } } require.Equal(t, expectedExisting, existing) require.Equal(t, expectedDeleted, deleted) - require.Equal(t, len(assetIdxs), existing+deleted) + require.Equal(t, len(creatableIdxs), existing+deleted) // Mine another maxBlocks blocks for i := 0; i < maxBlocks; i++ { @@ -460,21 +596,39 @@ func TestArchivalAssets(t *testing.T) { // deleted assets existing = 0 deleted = 0 - for aidx, status := range assetIdxs { - c, err := l.GetAssetCreator(aidx) - if status { + for aidx, status := range creatableIdxs { + switch status { + case AssetCreated: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) require.NoError(t, err) - require.Equal(t, assetCreators[aidx], c) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) existing++ - } else { - require.Error(t, err) + case AppCreated: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, allCreators[aidx], c) + existing++ + case AssetDeleted: + c, ok, err := l.GetCreator(aidx, basics.AssetCreatable) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, basics.Address{}, c) + deleted++ + case AppDeleted: + c, ok, err := l.GetCreator(aidx, basics.AppCreatable) + require.NoError(t, err) + require.False(t, ok) require.Equal(t, basics.Address{}, c) deleted++ + default: + panic("unknown action") } } require.Equal(t, expectedExisting, existing) require.Equal(t, expectedDeleted, deleted) - require.Equal(t, len(assetIdxs), existing+deleted) + require.Equal(t, len(creatableIdxs), existing+deleted) } func makeSignedTxnInBlock(tx transactions.Transaction) transactions.SignedTxnInBlock { diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index 68612d9163..c09f4ba4c5 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -310,7 +310,16 @@ func (c *CatchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte // if the account has any asset params, it means that it's the creator of an asset. if len(accountData.AssetParams) > 0 { for aidx := range accountData.AssetParams { - err = writeCatchpointStagingAssets(ctx, tx, balance.Address, aidx) + err = writeCatchpointStagingCreatable(ctx, tx, balance.Address, basics.CreatableIndex(aidx), basics.AssetCreatable) + if err != nil { + return err + } + } + } + + if len(accountData.AppParams) > 0 { + for aidx := range accountData.AppParams { + err = writeCatchpointStagingCreatable(ctx, tx, balance.Address, basics.CreatableIndex(aidx), basics.AppCreatable) if err != nil { return err } diff --git a/ledger/cow.go b/ledger/cow.go index 59bcb5be48..6d1e7bd661 100644 --- a/ledger/cow.go +++ b/ledger/cow.go @@ -17,8 +17,6 @@ package ledger import ( - "fmt" - "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" @@ -38,7 +36,7 @@ type roundCowParent interface { lookup(basics.Address) (basics.AccountData, error) isDup(basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) txnCounter() uint64 - getAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) + getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) } type roundCowState struct { @@ -59,8 +57,8 @@ type StateDelta struct { // new txleases for the txtail mapped to expiration txleases map[txlease]basics.Round - // new assets creator lookup table - assets map[basics.AssetIndex]modifiedAsset + // new creatables creator lookup table + creatables map[basics.CreatableIndex]modifiedCreatable // new block header; read-only hdr *bookkeeping.BlockHeader @@ -72,11 +70,11 @@ func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader) *roundCowS commitParent: nil, proto: config.Consensus[hdr.CurrentProtocol], mods: StateDelta{ - accts: make(map[basics.Address]accountDelta), - Txids: make(map[transactions.Txid]basics.Round), - txleases: make(map[txlease]basics.Round), - assets: make(map[basics.AssetIndex]modifiedAsset), - hdr: &hdr, + accts: make(map[basics.Address]accountDelta), + Txids: make(map[transactions.Txid]basics.Round), + txleases: make(map[txlease]basics.Round), + creatables: make(map[basics.CreatableIndex]modifiedCreatable), + hdr: &hdr, }, } } @@ -85,15 +83,15 @@ func (cb *roundCowState) rewardsLevel() uint64 { return cb.mods.hdr.RewardsLevel } -func (cb *roundCowState) getAssetCreator(aidx basics.AssetIndex) (basics.Address, error) { - delta, ok := cb.mods.assets[aidx] +func (cb *roundCowState) getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { + delta, ok := cb.mods.creatables[cidx] if ok { - if delta.created { - return delta.creator, nil + if delta.created && delta.ctype == ctype { + return delta.creator, true, nil } - return basics.Address{}, fmt.Errorf("asset %d has been deleted", aidx) + return basics.Address{}, false, nil } - return cb.lookupParent.getAssetCreator(aidx) + return cb.lookupParent.getCreator(cidx, ctype) } func (cb *roundCowState) lookup(addr basics.Address) (data basics.AccountData, err error) { @@ -125,7 +123,7 @@ func (cb *roundCowState) txnCounter() uint64 { return cb.lookupParent.txnCounter() + uint64(len(cb.mods.Txids)) } -func (cb *roundCowState) put(addr basics.Address, old basics.AccountData, new basics.AccountData) { +func (cb *roundCowState) put(addr basics.Address, old basics.AccountData, new basics.AccountData, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) { prev, present := cb.mods.accts[addr] if present { cb.mods.accts[addr] = accountDelta{old: prev.old, new: new} @@ -133,10 +131,20 @@ func (cb *roundCowState) put(addr basics.Address, old basics.AccountData, new ba cb.mods.accts[addr] = accountDelta{old: old, new: new} } - // Get which asset indices were created and deleted, and update state - assetDeltas := getChangedAssetIndices(addr, accountDelta{old: old, new: new}) - for aidx, delta := range assetDeltas { - cb.mods.assets[aidx] = delta + if newCreatable != nil { + cb.mods.creatables[newCreatable.Index] = modifiedCreatable{ + ctype: newCreatable.Type, + creator: newCreatable.Creator, + created: true, + } + } + + if deletedCreatable != nil { + cb.mods.creatables[deletedCreatable.Index] = modifiedCreatable{ + ctype: deletedCreatable.Type, + creator: deletedCreatable.Creator, + created: false, + } } } @@ -151,11 +159,11 @@ func (cb *roundCowState) child() *roundCowState { commitParent: cb, proto: cb.proto, mods: StateDelta{ - accts: make(map[basics.Address]accountDelta), - Txids: make(map[transactions.Txid]basics.Round), - txleases: make(map[txlease]basics.Round), - assets: make(map[basics.AssetIndex]modifiedAsset), - hdr: cb.mods.hdr, + accts: make(map[basics.Address]accountDelta), + Txids: make(map[transactions.Txid]basics.Round), + txleases: make(map[txlease]basics.Round), + creatables: make(map[basics.CreatableIndex]modifiedCreatable), + hdr: cb.mods.hdr, }, } } @@ -179,8 +187,8 @@ func (cb *roundCowState) commitToParent() { for txl, expires := range cb.mods.txleases { cb.commitParent.mods.txleases[txl] = expires } - for aidx, delta := range cb.mods.assets { - cb.commitParent.mods.assets[aidx] = delta + for cidx, delta := range cb.mods.creatables { + cb.commitParent.mods.creatables[cidx] = delta } } diff --git a/ledger/cow_test.go b/ledger/cow_test.go index 69444f96ec..5438e8e8d0 100644 --- a/ledger/cow_test.go +++ b/ledger/cow_test.go @@ -38,8 +38,16 @@ func (ml *mockLedger) isDup(firstValid, lastValid basics.Round, txn transactions return false, nil } -func (ml *mockLedger) getAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { - return basics.Address{}, nil +func (ml *mockLedger) getAssetCreator(assetIdx basics.AssetIndex) (basics.Address, bool, error) { + return basics.Address{}, false, nil +} + +func (ml *mockLedger) getAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { + return basics.Address{}, false, nil +} + +func (ml *mockLedger) getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + return basics.Address{}, false, nil } func (ml *mockLedger) txnCounter() uint64 { @@ -60,7 +68,7 @@ func checkCow(t *testing.T, cow *roundCowState, accts map[basics.Address]basics. func applyUpdates(cow *roundCowState, updates map[basics.Address]accountDelta) { for addr, delta := range updates { - cow.put(addr, delta.old, delta.new) + cow.put(addr, delta.old, delta.new, nil, nil) } } diff --git a/ledger/eval.go b/ledger/eval.go index 8d2765b84d..0c649a4e57 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/verify" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -61,8 +62,8 @@ type roundCowBase struct { proto config.ConsensusParams } -func (x *roundCowBase) getAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { - return x.l.GetAssetCreatorForRound(x.rnd, assetIdx) +func (x *roundCowBase) getCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + return x.l.GetCreatorForRound(x.rnd, cidx, ctype) } func (x *roundCowBase) lookup(addr basics.Address) (basics.AccountData, error) { @@ -77,18 +78,6 @@ func (x *roundCowBase) txnCounter() uint64 { return x.txnCount } -func (cs *roundCowState) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { - return cs.getAssetCreator(assetIdx) -} - -func (cs *roundCowState) GetAppCreator(appIdx basics.AppIndex) (basics.Address, bool, error) { - return basics.Address{}, false, errors.New("applications not implemented") -} - -func (cs *roundCowState) PutWithCreatables(record basics.BalanceRecord, newCreatables []basics.CreatableLocator, deletedCreatables []basics.CreatableLocator) error { - return errors.New("applications not implemented") -} - // wrappers for roundCowState to satisfy the (current) transactions.Balances interface func (cs *roundCowState) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { acctdata, err := cs.lookup(addr) @@ -101,12 +90,20 @@ func (cs *roundCowState) Get(addr basics.Address, withPendingRewards bool) (basi return basics.BalanceRecord{Addr: addr, AccountData: acctdata}, nil } +func (cs *roundCowState) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + return cs.getCreator(cidx, ctype) +} + func (cs *roundCowState) Put(record basics.BalanceRecord) error { + return cs.PutWithCreatable(record, nil, nil) +} + +func (cs *roundCowState) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { olddata, err := cs.lookup(record.Addr) if err != nil { return err } - cs.put(record.Addr, olddata, record.AccountData) + cs.put(record.Addr, olddata, record.AccountData, newCreatable, deletedCreatable) return nil } @@ -133,7 +130,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, fromBal, fromBalNew) + cs.put(from, fromBal, fromBalNew, nil, nil) toBal, err := cs.lookup(to) if err != nil { @@ -154,7 +151,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, toBal, toBalNew) + cs.put(to, toBal, toBalNew, nil, nil) return nil } @@ -177,6 +174,8 @@ type BlockEvaluator struct { block bookkeeping.Block blockTxBytes int + blockGenerated bool // prevent repeated GenerateBlock calls + l ledgerForEvaluator } @@ -188,7 +187,7 @@ type ledgerForEvaluator interface { isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) LookupWithoutRewards(basics.Round, basics.Address) (basics.AccountData, error) - GetAssetCreatorForRound(basics.Round, basics.AssetIndex) (basics.Address, error) + GetCreatorForRound(basics.Round, basics.CreatableIndex, basics.CreatableType) (basics.Address, bool, error) } // StartEvaluator creates a BlockEvaluator, given a ledger and a block header @@ -460,6 +459,45 @@ func (eval *BlockEvaluator) TransactionGroup(txads []transactions.SignedTxnWithA return eval.transactionGroup(txads) } +// prepareAppEvaluators creates appTealEvaluators for each ApplicationCall +// transaction in the group +func (eval *BlockEvaluator) prepareAppEvaluators(txgroup []transactions.SignedTxnWithAD) (res []*appTealEvaluator) { + var groupNoAD []transactions.SignedTxn + res = make([]*appTealEvaluator, len(txgroup)) + for i, txn := range txgroup { + // Ignore any non-ApplicationCall transactions + if txn.SignedTxn.Txn.Type != protocol.ApplicationCallTx { + continue + } + + // Initialize group without ApplyData lazily + if groupNoAD == nil { + groupNoAD = make([]transactions.SignedTxn, len(txgroup)) + for j := range txgroup { + groupNoAD[j] = txgroup[j].SignedTxn + } + } + + // Construct an appTealEvaluator (implements + // transactions.StateEvaluator) for use in ApplicationCall transactions. + steva := appTealEvaluator{ + evalParams: logic.EvalParams{ + Txn: &groupNoAD[i], + Proto: &eval.proto, + TxnGroup: groupNoAD, + GroupIndex: i, + }, + AppTealGlobals: AppTealGlobals{ + CurrentRound: eval.prevHeader.Round + 1, + LatestTimestamp: eval.prevHeader.TimeStamp, + }, + } + + res[i] = &steva + } + return +} + // transactionGroup tentatively executes a group of transactions as part of this block evaluation. // If the transaction group cannot be added to the block without violating some constraints, // an error is returned and the block evaluator state is unchanged. @@ -479,10 +517,15 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit cow := eval.state.child() + // Prepare TEAL contexts for any ApplicationCall transactions in the group + appEvaluators := eval.prepareAppEvaluators(txgroup) + + // Evaluate each transaction in the group + txibs = make([]transactions.SignedTxnInBlock, 0, len(txgroup)) for gi, txad := range txgroup { var txib transactions.SignedTxnInBlock - err := eval.transaction(txad.SignedTxn, txad.ApplyData, cow, &txib) + err := eval.transaction(txad.SignedTxn, appEvaluators[gi], txad.ApplyData, cow, &txib) if err != nil { return err } @@ -530,7 +573,7 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit // transaction tentatively executes a new transaction as part of this block evaluation. // If the transaction cannot be added to the block without violating some constraints, // an error is returned and the block evaluator state is unchanged. -func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transactions.ApplyData, cow *roundCowState, txib *transactions.SignedTxnInBlock) error { +func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, appEval *appTealEvaluator, ad transactions.ApplyData, cow *roundCowState, txib *transactions.SignedTxnInBlock) error { var err error // Only compute the TxID once @@ -572,7 +615,7 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti } // Apply the transaction, updating the cow balances - applyData, err := txn.Txn.Apply(cow, nil, spec, cow.txnCounter()) + applyData, err := txn.Txn.Apply(cow, appEval, spec, cow.txnCounter()) if err != nil { return fmt.Errorf("transaction %v: %v", txid, err) } @@ -622,10 +665,17 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti } dataNew := data.WithUpdatedRewards(eval.proto, rewardlvl) - effectiveMinBalance := basics.MulSaturate(eval.proto.MinBalance, uint64(1+len(dataNew.Assets))) - if dataNew.MicroAlgos.Raw < effectiveMinBalance { + effectiveMinBalance := dataNew.MinBalance(&eval.proto) + if dataNew.MicroAlgos.Raw < effectiveMinBalance.Raw { return fmt.Errorf("transaction %v: account %v balance %d below min %d (%d assets)", - txid, addr, dataNew.MicroAlgos.Raw, effectiveMinBalance, len(dataNew.Assets)) + txid, addr, dataNew.MicroAlgos.Raw, effectiveMinBalance.Raw, len(dataNew.Assets)) + } + + // Check if we have exceeded the maximum minimum balance + if eval.proto.MaximumMinimumBalance != 0 { + if effectiveMinBalance.Raw > eval.proto.MaximumMinimumBalance { + return fmt.Errorf("transaction %v: account %v would use too much space after this transaction. Minimum balance requirements would be %d (greater than max %d)", txid, addr, effectiveMinBalance.Raw, eval.proto.MaximumMinimumBalance) + } } } @@ -635,7 +685,7 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti return nil } -// Call "endOfBlock" after all the block's rewards and transactions are processed. Applies any deferred balance updates. +// Call "endOfBlock" after all the block's rewards and transactions are processed. func (eval *BlockEvaluator) endOfBlock() error { if eval.generate { eval.block.TxnRoot = eval.block.Payset.Commit(eval.proto.PaysetCommitFlat) @@ -673,11 +723,19 @@ func (eval *BlockEvaluator) finalValidation() error { // GenerateBlock produces a complete block from the BlockEvaluator. This is // used during proposal to get an actual block that will be proposed, after // feeding in tentative transactions into this block evaluator. +// +// After a call to GenerateBlock, the BlockEvaluator can still be used to +// accept transactions. However, to guard against reuse, subsequent calls +// to GenerateBlock on the same BlockEvaluator will fail. func (eval *BlockEvaluator) GenerateBlock() (*ValidatedBlock, error) { if !eval.generate { logging.Base().Panicf("GenerateBlock() called but generate is false") } + if eval.blockGenerated { + return nil, fmt.Errorf("GenerateBlock already called on this BlockEvaluator") + } + err := eval.endOfBlock() if err != nil { return nil, err @@ -692,6 +750,8 @@ func (eval *BlockEvaluator) GenerateBlock() (*ValidatedBlock, error) { blk: eval.block, delta: eval.state.mods, } + eval.blockGenerated = true + eval.state = makeRoundCowState(eval.state, eval.block.BlockHeader) return &vb, nil } diff --git a/ledger/eval_test.go b/ledger/eval_test.go index 0d8ac60c41..2a1b25e414 100644 --- a/ledger/eval_test.go +++ b/ledger/eval_test.go @@ -222,3 +222,99 @@ func TestRekeying(t *testing.T) { // TODO: More tests } + +func TestPrepareAppEvaluators(t *testing.T) { + eval := BlockEvaluator{ + prevHeader: bookkeeping.BlockHeader{ + TimeStamp: 1234, + Round: 2345, + }, + proto: config.ConsensusParams{ + Application: true, + }, + } + + // Create some sample transactions + payment := transactions.SignedTxnWithAD{ + SignedTxn: transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: basics.Address{1, 2, 3, 4}, + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: basics.Address{4, 3, 2, 1}, + Amount: basics.MicroAlgos{Raw: 100}, + }, + }, + }, + } + + appcall1 := transactions.SignedTxnWithAD{ + SignedTxn: transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: basics.Address{1, 2, 3, 4}, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: basics.AppIndex(1), + }, + }, + }, + } + + appcall2 := appcall1 + appcall2.SignedTxn.Txn.ApplicationCallTxnFields.ApplicationID = basics.AppIndex(2) + + type evalTestCase struct { + group []transactions.SignedTxnWithAD + + // indicates if prepareAppEvaluators should return a non-nil + // appTealEvaluator for the txn at index i + expected []bool + } + + // Create some groups with these transactions + cases := []evalTestCase{ + evalTestCase{[]transactions.SignedTxnWithAD{payment}, []bool{false}}, + evalTestCase{[]transactions.SignedTxnWithAD{appcall1}, []bool{true}}, + evalTestCase{[]transactions.SignedTxnWithAD{payment, payment}, []bool{false, false}}, + evalTestCase{[]transactions.SignedTxnWithAD{appcall1, payment}, []bool{true, false}}, + evalTestCase{[]transactions.SignedTxnWithAD{payment, appcall1}, []bool{false, true}}, + evalTestCase{[]transactions.SignedTxnWithAD{appcall1, appcall2}, []bool{true, true}}, + evalTestCase{[]transactions.SignedTxnWithAD{appcall1, appcall2, appcall1}, []bool{true, true, true}}, + evalTestCase{[]transactions.SignedTxnWithAD{payment, appcall1, payment}, []bool{false, true, false}}, + evalTestCase{[]transactions.SignedTxnWithAD{appcall1, payment, appcall2}, []bool{true, false, true}}, + } + + for i, testCase := range cases { + t.Run(fmt.Sprintf("i=%d", i), func(t *testing.T) { + res := eval.prepareAppEvaluators(testCase.group) + require.Equal(t, len(res), len(testCase.group)) + + // Compute the expected transaction group without ApplyData for + // the test case + expGroupNoAD := make([]transactions.SignedTxn, len(testCase.group)) + for j := range testCase.group { + expGroupNoAD[j] = testCase.group[j].SignedTxn + } + + // Ensure non app calls have a nil evaluator, and that non-nil + // evaluators point to the right transactions and values + for j, present := range testCase.expected { + if present { + require.NotNil(t, res[j]) + require.Equal(t, res[j].evalParams.GroupIndex, j) + require.Equal(t, res[j].evalParams.TxnGroup, expGroupNoAD) + require.Equal(t, *res[j].evalParams.Proto, eval.proto) + require.Equal(t, *res[j].evalParams.Txn, testCase.group[j].SignedTxn) + require.Equal(t, res[j].AppTealGlobals.CurrentRound, eval.prevHeader.Round+1) + require.Equal(t, res[j].AppTealGlobals.LatestTimestamp, eval.prevHeader.TimeStamp) + } else { + require.Nil(t, res[j]) + } + } + }) + } +} diff --git a/ledger/ledger.go b/ledger/ledger.go index 481fc9145c..a532c21028 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -320,25 +320,25 @@ func (l *Ledger) GetLastCatchpointLabel() string { return l.accts.getLastCatchpointLabel() } -// GetAssetCreatorForRound looks up the asset creator given the numerical asset -// ID. This is necessary so that we can retrieve the AssetParams from the -// creator's balance record. -func (l *Ledger) GetAssetCreatorForRound(rnd basics.Round, assetIdx basics.AssetIndex) (basics.Address, error) { +// GetCreatorForRound takes a CreatableIndex and a CreatableType and tries to +// look up a creator address, setting ok to false if the query succeeded but no +// creator was found. +func (l *Ledger) GetCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getAssetCreatorForRound(rnd, assetIdx) + return l.accts.getCreatorForRound(rnd, cidx, ctype) } -// GetAssetCreator is like GetAssetCreatorForRound, but for the latest round -// and race free with respect to ledger.Latest() -func (l *Ledger) GetAssetCreator(assetIdx basics.AssetIndex) (basics.Address, error) { +// GetCreator is like GetCreatorForRound, but for the latest round and race-free +// with respect to ledger.Latest() +func (l *Ledger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getAssetCreatorForRound(l.blockQ.latest(), assetIdx) + return l.accts.getCreatorForRound(l.blockQ.latest(), cidx, ctype) } // ListAssets takes a maximum asset index and maximum result length, and -// returns up to that many asset AssetIDs from the database where asset id is +// returns up to that many CreatableLocators from the database where app idx is // less than or equal to the maximum. func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { l.trackerMu.RLock() @@ -346,6 +346,15 @@ func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (r return l.accts.listAssets(maxAssetIdx, maxResults) } +// ListApplications takes a maximum app index and maximum result length, and +// returns up to that many CreatableLocators from the database where app idx is +// less than or equal to the maximum. +func (l *Ledger) ListApplications(maxAppIdx basics.AppIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { + l.trackerMu.RLock() + defer l.trackerMu.RUnlock() + return l.accts.listApplications(maxAppIdx, maxResults) +} + // Lookup uses the accounts tracker to return the account state for a // given account in a particular round. The account values reflect // the changes of all blocks up to and including rnd. diff --git a/ledger/ledger_perf_test.go b/ledger/ledger_perf_test.go new file mode 100644 index 0000000000..cc87acd01f --- /dev/null +++ b/ledger/ledger_perf_test.go @@ -0,0 +1,1297 @@ +// 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 . + +package ledger + +import ( + "context" + "crypto/rand" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/algorand/go-deadlock" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/data/transactions/verify" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" +) + +type testParams struct { + testType string + name string + program []byte + schemaSize uint64 + numApps uint64 + asaAccts uint64 +} + +var testCases map[string]testParams +var asaClearStateProgram []byte +var asaAppovalProgram []byte + +func makeUnsignedApplicationCallTxPerf(appIdx uint64, params testParams, onCompletion transactions.OnCompletion, round int) transactions.Transaction { + var tx transactions.Transaction + + tx.Type = protocol.ApplicationCallTx + tx.ApplicationID = basics.AppIndex(appIdx) + tx.OnCompletion = onCompletion + tx.Header.FirstValid = basics.Round(round) + tx.Header.LastValid = basics.Round(round + 1000) + tx.Header.Fee = basics.MicroAlgos{Raw: 1000} + + // If creating, set programs + if appIdx == 0 { + tx.ApprovalProgram = params.program + tx.ClearStateProgram = params.program + tx.GlobalStateSchema = basics.StateSchema{ + NumByteSlice: params.schemaSize, + } + tx.LocalStateSchema = basics.StateSchema{ + NumByteSlice: params.schemaSize, + } + } + + return tx +} + +func makeUnsignedASATx(appIdx uint64, creator basics.Address, round int) transactions.Transaction { + var tx transactions.Transaction + + tx.Type = protocol.ApplicationCallTx + tx.ApplicationID = basics.AppIndex(appIdx) + tx.Header.FirstValid = basics.Round(round) + tx.Header.LastValid = basics.Round(round + 1000) + tx.Header.Fee = basics.MicroAlgos{Raw: 1000} + + if appIdx == 0 { + tx.ApplicationArgs = [][]byte{ + creator[:], + creator[:], + creator[:], + creator[:], + creator[:], + []byte{0, 0, 0, 1, 0, 0, 0, 0}, + []byte{0, 0, 0, 0, 0, 0, 0, 0}, + } + tx.OnCompletion = transactions.NoOpOC + tx.ApprovalProgram = asaAppovalProgram + tx.ClearStateProgram = asaClearStateProgram + tx.GlobalStateSchema = basics.StateSchema{ + NumByteSlice: 5, + NumUint: 4, + } + tx.LocalStateSchema = basics.StateSchema{ + NumByteSlice: 0, + NumUint: 2, + } + } + return tx +} + +func makeUnsignedPaymentTx(sender basics.Address, round int) transactions.Transaction { + return transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + FirstValid: basics.Round(round), + LastValid: basics.Round(round + 1000), + Fee: basics.MicroAlgos{Raw: 1000}, + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: sender, + Amount: basics.MicroAlgos{Raw: 1234}, + }, + } +} + +type alwaysVerifiedCache struct{} + +func (vc *alwaysVerifiedCache) Verified(txn transactions.SignedTxn, params verify.Params) bool { + return true +} + +func benchmarkFullBlocks(params testParams, b *testing.B) { + // Disable deadlock checking library (we do this here and not in init + // because we want deadlock detection for unit tests) + deadlock.Opts.Disable = true + + dbTempDir, err := ioutil.TempDir("", "testdir"+b.Name()) + require.NoError(b, err) + dbName := fmt.Sprintf("%s.%d", b.Name(), crypto.RandUint64()) + dbPrefix := filepath.Join(dbTempDir, dbName) + defer os.RemoveAll(dbTempDir) + + genesisInitState := getInitState() + + // Use future protocol + genesisInitState.Block.BlockHeader.GenesisHash = crypto.Digest{} + genesisInitState.Block.CurrentProtocol = protocol.ConsensusFuture + genesisInitState.GenesisHash = crypto.Digest{1} + genesisInitState.Block.BlockHeader.GenesisHash = crypto.Digest{1} + + creator := basics.Address{} + _, err = rand.Read(creator[:]) + require.NoError(b, err) + genesisInitState.Accounts[creator] = basics.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1234567890}) + + // Make some accounts to opt into ASA + var accts []basics.Address + if params.testType == "asa" { + for i := uint64(0); i < params.asaAccts; i++ { + acct := basics.Address{} + _, err = rand.Read(acct[:]) + require.NoError(b, err) + genesisInitState.Accounts[acct] = basics.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1234567890}) + accts = append(accts, acct) + } + } + + // open first ledger + const inMem = false // use persistent storage + cfg := config.GetDefaultLocal() + cfg.Archival = true + l0, err := OpenLedger(logging.Base(), dbPrefix, inMem, genesisInitState, cfg) + require.NoError(b, err) + + // open second ledger + dbName = fmt.Sprintf("%s.%d.2", b.Name(), crypto.RandUint64()) + dbPrefix = filepath.Join(dbTempDir, dbName) + l1, err := OpenLedger(logging.Base(), dbPrefix, inMem, genesisInitState, cfg) + require.NoError(b, err) + + blk := genesisInitState.Block + + numBlocks := b.N + cert := agreement.Certificate{} + var blocks []bookkeeping.Block + var createdAppIdx uint64 + var txPerBlock int + onCompletion := transactions.OptInOC + for i := 0; i < numBlocks+2; i++ { + blk.BlockHeader.Round++ + blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) + blk.BlockHeader.GenesisID = "x" + + // If this is the zeroth block, add a blank one to both ledgers + if i == 0 { + err = l0.AddBlock(blk, cert) + require.NoError(b, err) + err = l1.AddBlock(blk, cert) + require.NoError(b, err) + continue + } + + // Construct evaluator for next block + prev, err := l0.BlockHdr(basics.Round(i)) + require.NoError(b, err) + newBlk := bookkeeping.MakeBlock(prev) + eval, err := l0.StartEvaluator(newBlk.BlockHeader, 5000) + require.NoError(b, err) + + // build a payset + var j int + for { + j++ + // make a transaction of the appropriate type + var tx transactions.Transaction + switch params.testType { + case "pay": + tx = makeUnsignedPaymentTx(creator, i) + case "app": + tx = makeUnsignedApplicationCallTxPerf(createdAppIdx, params, onCompletion, i) + case "asa": + tx = makeUnsignedASATx(createdAppIdx, creator, i) + // If we've created the ASA already, then fill in some spending parameters + if createdAppIdx != 0 { + // Creator spends to an opted in acct + tx.ApplicationArgs = [][]byte{ + []byte{0, 0, 0, 0, 0, 0, 0, 1}, + } + tx.Accounts = []basics.Address{ + accts[j%len(accts)], + basics.Address{}, + } + } + default: + panic("unknown tx type") + } + + numApps := uint64(1) + if i == 1 { + if params.numApps != 0 { + numApps = params.numApps + } + } + + // On first block, create params.numApps apps by adding numApps + // copies of the tx (with different notes fields). Otheriwse, just + // add 1. + for k := uint64(0); k < numApps; k++ { + tx.Sender = creator + tx.Note = []byte(fmt.Sprintf("%d,%d,%d", i, j, k)) + tx.GenesisHash = crypto.Digest{1} + + // add tx to block + var stxn transactions.SignedTxn + stxn.Txn = tx + stxn.Sig = crypto.Signature{1} + err = eval.Transaction(stxn, transactions.ApplyData{}) + + } + + // check if block is full + if err == ErrNoSpace { + txPerBlock = len(eval.block.Payset) + break + } else { + require.NoError(b, err) + } + + // First block just creates app + opts in accts if asa test + if i == 1 { + onCompletion = transactions.NoOpOC + createdAppIdx = eval.state.txnCounter() + + // On first block, opt in all accts to asa (accts is empty if not asa test) + k := 0 + for _, acct := range accts { + tx = makeUnsignedASATx(createdAppIdx, basics.Address{}, i) + tx.OnCompletion = transactions.OptInOC + tx.Sender = acct + tx.Note = []byte(fmt.Sprintf("%d,%d,%d", i, j, k)) + tx.GenesisHash = crypto.Digest{1} + k++ + + // add tx to block + var stxn transactions.SignedTxn + stxn.Txn = tx + stxn.Sig = crypto.Signature{1} + err = eval.Transaction(stxn, transactions.ApplyData{}) + } + break + } + } + + lvb, err := eval.GenerateBlock() + require.NoError(b, err) + + // If this is the app creation block, add to both ledgers + if i == 1 { + err = l0.AddBlock(lvb.blk, cert) + require.NoError(b, err) + err = l1.AddBlock(lvb.blk, cert) + require.NoError(b, err) + continue + } + + // For all other blocks, add just to the first ledger, and stash + // away to be replayed in the second ledger while running timer + err = l0.AddBlock(lvb.blk, cert) + require.NoError(b, err) + + blocks = append(blocks, lvb.blk) + } + + b.Logf("built %d blocks, each with %d txns", numBlocks, txPerBlock) + + // eval + add all the (valid) blocks to the second ledger, measuring it this time + vc := alwaysVerifiedCache{} + b.ResetTimer() + for _, blk := range blocks { + _, err = l1.eval(context.Background(), blk, true, &vc, nil) + require.NoError(b, err) + err = l1.AddBlock(blk, cert) + require.NoError(b, err) + } +} + +func BenchmarkAppLocal1NoDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-local-1-no-diffs"], b) +} + +func BenchmarkAppLocal16NoDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-local-16-no-diffs"], b) +} + +func BenchmarkAppGlobal1NoDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-global-1-no-diffs"], b) +} + +func BenchmarkAppGlobal16NoDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-global-16-no-diffs"], b) +} + +func BenchmarkAppLocal1BigDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-local-1-big-diffs"], b) +} + +func BenchmarkAppLocal16BigDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-local-16-big-diffs"], b) +} + +func BenchmarkAppGlobal1BigDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-global-1-big-diffs"], b) +} + +func BenchmarkAppGlobal16BigDiffs(b *testing.B) { + benchmarkFullBlocks(testCases["bench-global-16-big-diffs"], b) +} + +func BenchmarkAppGlobal64MaxClone(b *testing.B) { + benchmarkFullBlocks(testCases["bench-global-64-max-clone"], b) +} + +func BenchmarkAppInt1(b *testing.B) { benchmarkFullBlocks(testCases["int-1"], b) } + +func BenchmarkAppInt1ManyApps(b *testing.B) { benchmarkFullBlocks(testCases["int-1-many-apps"], b) } + +func BenchmarkAppBigNoOp(b *testing.B) { benchmarkFullBlocks(testCases["big-noop"], b) } + +func BenchmarkAppBigHashes(b *testing.B) { benchmarkFullBlocks(testCases["big-hashes"], b) } + +func BenchmarkAppASA(b *testing.B) { benchmarkFullBlocks(testCases["asa"], b) } + +func BenchmarkPay(b *testing.B) { benchmarkFullBlocks(testCases["pay"], b) } + +func init() { + testCases = make(map[string]testParams) + + lengths := []int{1, 16} + diffs := []bool{true, false} + state := []string{"local", "global"} + + for _, l := range lengths { + for _, d := range diffs { + for _, s := range state { + params := genAppTestParams(l, d, s) + testCases[params.name] = params + } + } + } + + // Max clone + params := genAppTestParamsMaxClone(64) + testCases[params.name] = params + + // Int 1 + progBytes, err := logic.AssembleStringV2(`int 1`) + if err != nil { + panic(err) + } + + params = testParams{ + testType: "app", + name: fmt.Sprintf("int-1"), + program: progBytes, + } + testCases[params.name] = params + + // Int 1 many apps + params = testParams{ + testType: "app", + name: fmt.Sprintf("int-1-many-apps"), + program: progBytes, + numApps: 10, + } + testCases[params.name] = params + + // Assemble ASA programs + asaClearStateProgram, err = logic.AssembleStringV2(asaClearAsm) + if err != nil { + panic(err) + } + + asaAppovalProgram, err = logic.AssembleStringV2(asaAppovalAsm) + if err != nil { + panic(err) + } + + // ASAs + params = testParams{ + testType: "asa", + name: "asa", + asaAccts: 100, + numApps: 10, + } + testCases[params.name] = params + + // Payments + params = testParams{ + testType: "pay", + name: "pay", + } + testCases[params.name] = params + + // Big NoOp + params = testParams{ + testType: "app", + name: "big-noop", + program: genBigNoOp(696), + } + testCases[params.name] = params + + // Big hashes + params = testParams{ + testType: "app", + name: "big-hashes", + program: genBigHashes(10, 344), + } + testCases[params.name] = params +} + +func genBigNoOp(numOps int) []byte { + var progParts []string + for i := 0; i < numOps/2; i++ { + progParts = append(progParts, `int 1`) + progParts = append(progParts, `pop`) + } + progParts = append(progParts, `int 1`) + progParts = append(progParts, `return`) + progAsm := strings.Join(progParts, "\n") + progBytes, err := logic.AssembleStringV2(progAsm) + if err != nil { + panic(err) + } + return progBytes +} + +func genBigHashes(numHashes int, numPad int) []byte { + var progParts []string + progParts = append(progParts, `byte base64 AA==`) + for i := 0; i < numHashes; i++ { + progParts = append(progParts, `sha256`) + } + for i := 0; i < numPad/2; i++ { + progParts = append(progParts, `int 1`) + progParts = append(progParts, `pop`) + } + progParts = append(progParts, `int 1`) + progParts = append(progParts, `return`) + progAsm := strings.Join(progParts, "\n") + progBytes, err := logic.AssembleStringV2(progAsm) + if err != nil { + panic(err) + } + return progBytes +} + +func genAppTestParams(numKeys int, bigDiffs bool, stateType string) testParams { + var deleteBranch string + var writePrefix, writeBlock, writeSuffix string + var deletePrefix, deleteBlock, deleteSuffix string + + switch stateType { + case "local": + // goto delete if first key exists + deleteBranch = ` + int 0 + int 0 + int 1 + itob + app_local_get_ex + bnz delete + ` + + writePrefix = ` + write: + int 0 + store 0 + ` + + writeBlock = ` + int 0 + load 0 + int 1 + + + dup + store 0 + itob + dup + app_local_put + ` + + writeSuffix = ` + int 1 + return + ` + + deletePrefix = ` + delete: + int 0 + store 0 + ` + + deleteBlock = ` + int 0 + load 0 + int 1 + + + dup + store 0 + itob + app_local_del + ` + + deleteSuffix = ` + int 1 + return + ` + case "global": + // goto delete if first key exists + deleteBranch = ` + int 0 // current app id + int 1 // key + itob + app_global_get_ex + bnz delete + ` + + writePrefix = ` + write: + int 0 + ` + + writeBlock = ` + int 1 + + + dup + itob + dup + app_global_put + ` + + writeSuffix = ` + int 1 + return + ` + + deletePrefix = ` + delete: + int 0 + ` + + deleteBlock = ` + int 1 + + + dup + itob + app_global_del + ` + + deleteSuffix = ` + int 1 + return + ` + default: + panic("unknown state type") + } + + testDiffName := "big-diffs" + if !bigDiffs { + deleteBranch = `` + deletePrefix = `` + deleteBlock = `` + deleteSuffix = `` + testDiffName = "no-diffs" + } + + // generate assembly + progParts := []string{"#pragma version 2"} + progParts = append(progParts, deleteBranch) + progParts = append(progParts, writePrefix) + for i := 0; i < numKeys; i++ { + progParts = append(progParts, writeBlock) + } + progParts = append(progParts, writeSuffix) + progParts = append(progParts, deletePrefix) + for i := 0; i < numKeys; i++ { + progParts = append(progParts, deleteBlock) + } + progParts = append(progParts, deleteSuffix) + progAsm := strings.Join(progParts, "\n") + + // assemble + progBytes, err := logic.AssembleStringV2(progAsm) + if err != nil { + panic(err) + } + + return testParams{ + testType: "app", + name: fmt.Sprintf("bench-%s-%d-%s", stateType, numKeys, testDiffName), + schemaSize: uint64(numKeys), + program: progBytes, + } +} + +func genAppTestParamsMaxClone(numKeys int) testParams { + // goto flip if first key exists + flipBranch := ` + int 0 // current app id + int 1 // key + itob + app_global_get_ex + bnz flip + ` + + writePrefix := ` + write: + int 0 + ` + + writeBlock := ` + int 1 + + + dup + itob + dup + app_global_put + ` + + writeSuffix := ` + int 1 + return + ` + + // flip stored value's low bit + flipPrefix := ` + flip: + btoi + int 1 + ^ + itob + store 0 + int 1 + itob + load 0 + app_global_put + ` + + flipSuffix := ` + int 1 + return + ` + + testDiffName := "max-clone" + + // generate assembly + progParts := []string{"#pragma version 2"} + progParts = append(progParts, flipBranch) + progParts = append(progParts, writePrefix) + for i := 0; i < numKeys; i++ { + progParts = append(progParts, writeBlock) + } + progParts = append(progParts, writeSuffix) + progParts = append(progParts, flipPrefix) + progParts = append(progParts, flipSuffix) + progAsm := strings.Join(progParts, "\n") + + // assemble + progBytes, err := logic.AssembleStringV2(progAsm) + if err != nil { + panic(err) + } + + return testParams{ + testType: "app", + name: fmt.Sprintf("bench-%s-%d-%s", "global", numKeys, testDiffName), + schemaSize: uint64(numKeys), + program: progBytes, + } +} + +const asaClearAsm = `#pragma version 2 +byte base64 Ymw= +byte base64 Ymw= +app_global_get +int 0 +int 0 +byte base64 Ymw= +app_local_get_ex +pop ++ +app_global_put +int 1 +` + +const asaAppovalAsm = `#pragma version 2 +txn NumAppArgs +int 7 +== +bnz if0 +txn ApplicationID +int 0 +== +! +bnz assert2 +err +assert2: +txn NumAccounts +int 0 +== +bnz cond4 +txn NumAccounts +int 1 +== +bnz cond5 +txn NumAppArgs +int 2 +== +bnz cond6 +// transfer asset +txna ApplicationArgs 0 +btoi +store 1 +load 1 +int 0 +== +bnz unless7 +// cannot modify frozen asset +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if9 +int 0 +int 0 +byte base64 Zno= +app_local_get_ex +pop +int 1 +== +int 1 +bnz if_end10 +if9: +byte base64 Zno= +app_global_get +int 1 +== +if_end10: +! +bnz assert8 +err +assert8: +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if11 +int 0 +byte base64 Ymw= +int 0 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 1 +- +app_local_put +int 1 +bnz if_end12 +if11: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 1 +- +app_global_put +if_end12: +unless7: +load 1 +int 0 +== +bnz unless13 +// cannot modify frozen asset +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if15 +int 1 +int 0 +byte base64 Zno= +app_local_get_ex +pop +int 1 +== +int 1 +bnz if_end16 +if15: +byte base64 Zno= +app_global_get +int 1 +== +if_end16: +! +bnz assert14 +err +assert14: +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if17 +int 1 +byte base64 Ymw= +int 1 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 1 ++ +app_local_put +int 1 +bnz if_end18 +if17: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 1 ++ +app_global_put +if_end18: +unless13: +txna Accounts 2 +global ZeroAddress +== +bnz unless19 +int 0 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +store 2 +load 2 +int 0 +== +bnz unless20 +// cannot modify frozen asset +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if22 +int 0 +int 0 +byte base64 Zno= +app_local_get_ex +pop +int 1 +== +int 1 +bnz if_end23 +if22: +byte base64 Zno= +app_global_get +int 1 +== +if_end23: +! +bnz assert21 +err +assert21: +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if24 +int 0 +byte base64 Ymw= +int 0 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 2 +- +app_local_put +int 1 +bnz if_end25 +if24: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 2 +- +app_global_put +if_end25: +unless20: +load 2 +int 0 +== +bnz unless26 +// cannot modify frozen asset +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if28 +int 2 +int 0 +byte base64 Zno= +app_local_get_ex +pop +int 1 +== +int 1 +bnz if_end29 +if28: +byte base64 Zno= +app_global_get +int 1 +== +if_end29: +! +bnz assert27 +err +assert27: +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if30 +int 2 +byte base64 Ymw= +int 2 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 2 ++ +app_local_put +int 1 +bnz if_end31 +if30: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 2 ++ +app_global_put +if_end31: +unless26: +unless19: +txn NumAppArgs +int 1 +== +txn NumAccounts +int 2 +== +&& +txn OnCompletion +int 0 +== +bnz if32 +txn OnCompletion +int 2 +== +int 0 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +int 0 +== +&& +txna Accounts 2 +global ZeroAddress +== +! +&& +int 1 +bnz if_end33 +if32: +txna Accounts 2 +global ZeroAddress +== +if_end33: +&& +int 1 +bnz cond_end3 +cond6: +// clawback asset +txna ApplicationArgs 0 +btoi +store 0 +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if34 +int 1 +byte base64 Ymw= +int 1 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 0 +- +app_local_put +int 1 +bnz if_end35 +if34: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 0 +- +app_global_put +if_end35: +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if36 +int 2 +byte base64 Ymw= +int 2 +int 0 +byte base64 Ymw= +app_local_get_ex +pop +load 0 ++ +app_local_put +int 1 +bnz if_end37 +if36: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 0 ++ +app_global_put +if_end37: +txn NumAccounts +int 2 +== +txn OnCompletion +int 0 +== +&& +txn Sender +byte base64 Y2w= +app_global_get +== +&& +int 1 +bnz cond_end3 +cond5: +// freeze asset holding +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if38 +int 1 +byte base64 Zno= +txna ApplicationArgs 0 +btoi +app_local_put +int 1 +bnz if_end39 +if38: +byte base64 Zno= +txna ApplicationArgs 0 +btoi +app_global_put +if_end39: +txn NumAppArgs +int 1 +== +txn OnCompletion +int 0 +== +&& +txn Sender +byte base64 ZnI= +app_global_get +== +&& +int 1 +bnz cond_end3 +cond4: +// asset deletion or opt-in +txn OnCompletion +int 1 +== +! +bnz when40 +// opting in to implicit zero bl +int 0 +byte base64 Zno= +byte base64 ZGY= +app_global_get +app_local_put +when40: +txn NumAppArgs +int 0 +== +txn OnCompletion +int 5 +== +txn Sender +byte base64 bW4= +app_global_get +== +&& +byte base64 dHQ= +app_global_get +byte base64 Ymw= +app_global_get +== +&& +txn OnCompletion +int 1 +== +txn Sender +byte base64 Y3I= +app_global_get +== +! +&& +|| +&& +cond_end3: +int 1 +bnz if_end1 +if0: +// asset configuration +txn ApplicationID +int 0 +== +bnz if41 +txn Sender +byte base64 bW4= +app_global_get +== +txna ApplicationArgs 0 +global ZeroAddress +== +byte base64 bW4= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 1 +global ZeroAddress +== +byte base64 cnY= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 2 +global ZeroAddress +== +byte base64 ZnI= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 3 +global ZeroAddress +== +byte base64 Y2w= +app_global_get +global ZeroAddress +== +! +|| +&& +bnz assert43 +err +assert43: +int 1 +bnz if_end42 +if41: +byte base64 Y3I= +txna ApplicationArgs 4 +app_global_put +byte base64 dHQ= +txna ApplicationArgs 5 +btoi +app_global_put +byte base64 Ymw= +txna ApplicationArgs 5 +btoi +app_global_put +byte base64 ZGY= +txna ApplicationArgs 6 +btoi +app_global_put +if_end42: +byte base64 bW4= +txna ApplicationArgs 0 +app_global_put +byte base64 cnY= +txna ApplicationArgs 1 +app_global_put +byte base64 ZnI= +txna ApplicationArgs 2 +app_global_put +byte base64 Y2w= +txna ApplicationArgs 3 +app_global_put +txn NumAccounts +int 0 +== +txn OnCompletion +int 0 +== +&& +txna ApplicationArgs 0 +len +int 32 +== +&& +txna ApplicationArgs 1 +len +int 32 +== +&& +txna ApplicationArgs 2 +len +int 32 +== +&& +txna ApplicationArgs 3 +len +int 32 +== +&& +if_end1: +` diff --git a/libgoal/libgoal.go b/libgoal/libgoal.go index c51dc8f07c..1b465a134b 100644 --- a/libgoal/libgoal.go +++ b/libgoal/libgoal.go @@ -17,18 +17,20 @@ package libgoal import ( + "encoding/json" "fmt" "os" "path/filepath" algodclient "github.com/algorand/go-algorand/daemon/algod/api/client" + v2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2" generatedV2 "github.com/algorand/go-algorand/daemon/algod/api/server/v2/generated" kmdclient "github.com/algorand/go-algorand/daemon/kmd/client" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/daemon/algod/api/spec/common" - "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/daemon/kmd/lib/kmdapi" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" @@ -649,6 +651,28 @@ func (c *Client) AccountInformation(account string) (resp v1.Account, err error) return } +// AccountInformationV2 takes an address and returns its information +func (c *Client) AccountInformationV2(account string) (resp generatedV2.Account, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + resp, err = algod.AccountInformationV2(account) + } + return +} + +// AccountData takes an address and returns its basics.AccountData +func (c *Client) AccountData(account string) (accountData basics.AccountData, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + var resp []byte + resp, err = algod.RawAccountInformationV2(account) + if err == nil { + err = protocol.Decode(resp, &accountData) + } + } + return +} + // AssetInformation takes an asset's index and returns its information func (c *Client) AssetInformation(index uint64) (resp v1.AssetParams, err error) { algod, err := c.ensureAlgodClient() @@ -658,6 +682,24 @@ func (c *Client) AssetInformation(index uint64) (resp v1.AssetParams, err error) return } +// AssetInformationV2 takes an asset's index and returns its information +func (c *Client) AssetInformationV2(index uint64) (resp generatedV2.Asset, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + resp, err = algod.AssetInformationV2(index) + } + return +} + +// ApplicationInformation takes an app's index and returns its information +func (c *Client) ApplicationInformation(index uint64) (resp generatedV2.Application, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + resp, err = algod.ApplicationInformation(index) + } + return +} + // TransactionInformation takes an address and associated txid and return its information func (c *Client) TransactionInformation(addr, txid string) (resp v1.Transaction, err error) { algod, err := c.ensureAlgodClient() @@ -859,3 +901,125 @@ func (c *Client) Catchup(catchpointLabel string) error { } return nil } + +const defaultAppIdx = 1380011588 + +// MakeDryrunStateBytes function creates DryrunRequest data structure in serialized form according to the format +func MakeDryrunStateBytes(client Client, txnOrStxn interface{}, other []transactions.SignedTxn, proto string, format string) (result []byte, err error) { + switch format { + case "json": + var gdr generatedV2.DryrunRequest + gdr, err = MakeDryrunStateGenerated(client, txnOrStxn, other, proto) + if err == nil { + result = protocol.EncodeJSON(&gdr) + } + return + case "msgp": + var dr v2.DryrunRequest + dr, err = MakeDryrunState(client, txnOrStxn, other, proto) + if err == nil { + result = protocol.EncodeReflect(&dr) + } + return + default: + return nil, fmt.Errorf("format %s not supported", format) + } +} + +// MakeDryrunState function creates DryrunRequest data structure and serializes it into a file +func MakeDryrunState(client Client, txnOrStxn interface{}, other []transactions.SignedTxn, proto string) (dr v2.DryrunRequest, err error) { + gdr, err := MakeDryrunStateGenerated(client, txnOrStxn, other, proto) + if err != nil { + return + } + return v2.DryrunRequestFromGenerated(&gdr) +} + +// MakeDryrunStateGenerated function creates DryrunRequest data structure and serializes it into a file +func MakeDryrunStateGenerated(client Client, txnOrStxn interface{}, other []transactions.SignedTxn, proto string) (dr generatedV2.DryrunRequest, err error) { + var txns []transactions.SignedTxn + var tx *transactions.Transaction + if txn, ok := txnOrStxn.(transactions.Transaction); ok { + tx = &txn + txns = append(txns, transactions.SignedTxn{Txn: txn}) + } else if stxn, ok := txnOrStxn.(transactions.SignedTxn); ok { + tx = &stxn.Txn + txns = append(txns, stxn) + } else { + err = fmt.Errorf("unsupported txn type") + return + } + txns = append(txns, other...) + for i := range txns { + enc := protocol.EncodeJSON(&txns[i]) + dr.Txns = append(dr.Txns, enc) + } + + if tx.Type == protocol.ApplicationCallTx { + apps := []basics.AppIndex{tx.ApplicationID} + apps = append(apps, tx.ForeignApps...) + for _, appIdx := range apps { + var appParams generatedV2.ApplicationParams + if appIdx == 0 { + // if it is an app create txn then use params from the txn + appParams.ApprovalProgram = tx.ApprovalProgram + appParams.ClearStateProgram = tx.ClearStateProgram + appParams.GlobalStateSchema = &generatedV2.ApplicationStateSchema{ + NumUint: tx.GlobalStateSchema.NumUint, + NumByteSlice: tx.GlobalStateSchema.NumByteSlice, + } + appParams.LocalStateSchema = &generatedV2.ApplicationStateSchema{ + NumUint: tx.LocalStateSchema.NumUint, + NumByteSlice: tx.LocalStateSchema.NumByteSlice, + } + appParams.Creator = tx.Sender.String() + // zero is not acceptable by ledger in dryrun/debugger + appIdx = defaultAppIdx + } else { + // otherwise need to fetch app state + var app generatedV2.Application + if app, err = client.ApplicationInformation(uint64(tx.ApplicationID)); err != nil { + return + } + appParams = app.Params + } + dr.Apps = append(dr.Apps, generatedV2.Application{ + Id: uint64(appIdx), + Params: appParams, + }) + } + + accounts := append(tx.Accounts, tx.Sender) + for _, acc := range accounts { + var info generatedV2.Account + if info, err = client.AccountInformationV2(acc.String()); err != nil { + return + } + dr.Accounts = append(dr.Accounts, info) + } + + dr.ProtocolVersion = proto + if dr.Round, err = client.CurrentRound(); err != nil { + return + } + var b v1.Block + if b, err = client.Block(dr.Round); err != nil { + return + } + dr.LatestTimestamp = uint64(b.Timestamp) + } + return +} + +// Dryrun takes an app's index and returns its information +func (c *Client) Dryrun(data []byte) (resp generatedV2.DryrunResponse, err error) { + algod, err := c.ensureAlgodClient() + if err == nil { + data, err = algod.RawDryrun(data) + if err != nil { + return + } + err = json.Unmarshal(data, &resp) + } + return +} diff --git a/libgoal/transactions.go b/libgoal/transactions.go index 6d833c3a54..514ce5d5e8 100644 --- a/libgoal/transactions.go +++ b/libgoal/transactions.go @@ -28,6 +28,8 @@ import ( "github.com/algorand/go-algorand/protocol" ) +var emptySchema = basics.StateSchema{} + // SignTransactionWithWallet signs the passed transaction with keys from the wallet associated with the passed walletHandle func (c *Client) SignTransactionWithWallet(walletHandle, pw []byte, utx transactions.Transaction) (stx transactions.SignedTxn, err error) { kmd, err := c.ensureKmdClient() @@ -384,6 +386,88 @@ func (c *Client) FillUnsignedTxTemplate(sender string, firstValid, lastValid, fe return tx, nil } +// MakeUnsignedAppCreateTx makes a transaction for creating an application +func (c *Client) MakeUnsignedAppCreateTx(onComplete transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(0, appArgs, accounts, foreignApps, onComplete, approvalProg, clearProg, globalSchema, localSchema) +} + +// MakeUnsignedAppUpdateTx makes a transaction for updating an application's programs +func (c *Client) MakeUnsignedAppUpdateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, approvalProg []byte, clearProg []byte) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.UpdateApplicationOC, approvalProg, clearProg, emptySchema, emptySchema) +} + +// MakeUnsignedAppDeleteTx makes a transaction for deleting an application +func (c *Client) MakeUnsignedAppDeleteTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.DeleteApplicationOC, nil, nil, emptySchema, emptySchema) +} + +// MakeUnsignedAppOptInTx makes a transaction for opting in to (allocating +// some account-specific state for) an application +func (c *Client) MakeUnsignedAppOptInTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.OptInOC, nil, nil, emptySchema, emptySchema) +} + +// MakeUnsignedAppCloseOutTx makes a transaction for closing out of +// (deallocating all account-specific state for) an application +func (c *Client) MakeUnsignedAppCloseOutTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.CloseOutOC, nil, nil, emptySchema, emptySchema) +} + +// MakeUnsignedAppClearStateTx makes a transaction for clearing out all +// account-specific state for an application. It may not be rejected by the +// application's logic. +func (c *Client) MakeUnsignedAppClearStateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.ClearStateOC, nil, nil, emptySchema, emptySchema) +} + +// MakeUnsignedAppNoOpTx makes a transaction for interacting with an existing +// application, potentially updating any account-specific local state and +// global state associated with it. +func (c *Client) MakeUnsignedAppNoOpTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.NoOpOC, nil, nil, emptySchema, emptySchema) +} + +// MakeUnsignedApplicationCallTx is a helper for the above ApplicationCall +// transaction constructors. A fully custom ApplicationCall transaction may +// be constructed using this method. +func (c *Client) MakeUnsignedApplicationCallTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, onCompletion transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema) (tx transactions.Transaction, err error) { + tx.Type = protocol.ApplicationCallTx + tx.ApplicationID = basics.AppIndex(appIdx) + tx.OnCompletion = onCompletion + + tx.ApplicationArgs = appArgs + tx.Accounts, err = parseTxnAccounts(accounts) + if err != nil { + return tx, err + } + + tx.ForeignApps = parseTxnForeignApps(foreignApps) + tx.ApprovalProgram = approvalProg + tx.ClearStateProgram = clearProg + tx.LocalStateSchema = localSchema + tx.GlobalStateSchema = globalSchema + + return tx, nil +} + +func parseTxnAccounts(accounts []string) (parsed []basics.Address, err error) { + for _, acct := range accounts { + addr, err := basics.UnmarshalChecksumAddress(acct) + if err != nil { + return nil, err + } + parsed = append(parsed, addr) + } + return +} + +func parseTxnForeignApps(foreignApps []uint64) (parsed []basics.AppIndex) { + for _, aidx := range foreignApps { + parsed = append(parsed, basics.AppIndex(aidx)) + } + return +} + // MakeUnsignedAssetCreateTx creates a tx template for creating // an asset. // diff --git a/shared/pingpong/accounts.go b/shared/pingpong/accounts.go index f2063c5143..aa6824ba00 100644 --- a/shared/pingpong/accounts.go +++ b/shared/pingpong/accounts.go @@ -18,14 +18,18 @@ package pingpong import ( "fmt" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" - "github.com/algorand/go-algorand/data/transactions" - "github.com/algorand/go-algorand/libgoal" "math" "os" "sort" + "strings" "time" + + "github.com/algorand/go-algorand/crypto" + v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + "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/libgoal" ) func ensureAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]uint64, cfg PpConfig, err error) { @@ -88,7 +92,6 @@ func ensureAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]ui // Only reuse existing accounts for non asset testing. // For asset testing, new participant accounts will be created since accounts are limited to 1000 assets. if cfg.NumAsset == 0 { - // If we have more accounts than requested, pick the top N (not including src) if len(accounts) > int(cfg.NumPartAccounts+1) { fmt.Printf("Finding the richest %d accounts to use for transacting\n", cfg.NumPartAccounts) @@ -315,6 +318,193 @@ func signAndBroadcastTransaction(accounts map[string]uint64, sender string, tx t return } +func genBigNoOp(numOps uint32) []byte { + var progParts []string + for i := uint32(0); i < numOps/2; i++ { + progParts = append(progParts, `int 1`) + progParts = append(progParts, `pop`) + } + progParts = append(progParts, `int 1`) + progParts = append(progParts, `return`) + progAsm := strings.Join(progParts, "\n") + progBytes, err := logic.AssembleString(progAsm) + if err != nil { + panic(err) + } + return progBytes +} + +func genBigHashes(numHashes int, numPad int, hash string) []byte { + var progParts []string + progParts = append(progParts, `byte base64 AA==`) + for i := 0; i < numHashes; i++ { + progParts = append(progParts, hash) + } + for i := 0; i < numPad/2; i++ { + progParts = append(progParts, `int 1`) + progParts = append(progParts, `pop`) + } + progParts = append(progParts, `int 1`) + progParts = append(progParts, `return`) + progAsm := strings.Join(progParts, "\n") + progBytes, err := logic.AssembleString(progAsm) + if err != nil { + panic(err) + } + return progBytes +} + +func genMaxClone(numKeys int) []byte { + // goto flip if first key exists + flipBranch := ` + int 0 // current app id + int 1 // key + itob + app_global_get_ex + bnz flip + ` + + writePrefix := ` + write: + int 0 + ` + + writeBlock := ` + int 1 + + + dup + itob + dup + app_global_put + ` + + writeSuffix := ` + int 1 + return + ` + + // flip stored value's low bit + flipPrefix := ` + flip: + btoi + int 1 + ^ + itob + store 0 + int 1 + itob + load 0 + app_global_put + ` + + flipSuffix := ` + int 1 + return + ` + + // generate assembly + progParts := []string{"#pragma version 2"} + progParts = append(progParts, flipBranch) + progParts = append(progParts, writePrefix) + for i := 0; i < numKeys; i++ { + progParts = append(progParts, writeBlock) + } + progParts = append(progParts, writeSuffix) + progParts = append(progParts, flipPrefix) + progParts = append(progParts, flipSuffix) + progAsm := strings.Join(progParts, "\n") + + // assemble + progBytes, err := logic.AssembleString(progAsm) + if err != nil { + panic(err) + } + return progBytes +} + +func prepareApps(accounts map[string]uint64, client libgoal.Client, cfg PpConfig) (appParams map[uint64]v1.AppParams, err error) { + // get existing apps + account, accountErr := client.AccountInformation(cfg.SrcAccount) + if accountErr != nil { + fmt.Printf("Cannot lookup source account") + err = accountErr + return + } + + // Get wallet handle token + var h []byte + h, err = client.GetUnencryptedWalletHandle() + if err != nil { + return + } + + toCreate := int(cfg.NumApp) - len(account.AppParams) + + // create apps in srcAccount + for i := 0; i < toCreate; i++ { + var tx transactions.Transaction + + // generate app program with roughly some number of operations + prog := genBigNoOp(cfg.AppProgOps) + + globSchema := basics.StateSchema{NumByteSlice: 64} + locSchema := basics.StateSchema{} + tx, err = client.MakeUnsignedAppCreateTx(transactions.NoOpOC, prog, prog, globSchema, locSchema, nil, nil, nil) + if err != nil { + fmt.Printf("Cannot create app txn\n") + return + } + + tx, err = client.FillUnsignedTxTemplate(cfg.SrcAccount, 0, 0, cfg.MaxFee, tx) + if err != nil { + fmt.Printf("Cannot fill app creation txn\n") + return + } + + // Ensure different txids + var note [8]byte + crypto.RandBytes(note[:]) + tx.Note = note[:] + + signedTxn, signErr := client.SignTransactionWithWallet(h, nil, tx) + if signErr != nil { + fmt.Printf("Cannot sign app creation txn\n") + err = signErr + return + } + + txid, broadcastErr := client.BroadcastTransaction(signedTxn) + if broadcastErr != nil { + fmt.Printf("Cannot broadcast app creation txn\n") + err = broadcastErr + return + } + + if !cfg.Quiet { + fmt.Printf("Create a new app: txid=%s\n", txid) + } + + accounts[cfg.SrcAccount] -= tx.Fee.Raw + } + + // get these apps + for { + account, accountErr = client.AccountInformation(cfg.SrcAccount) + if accountErr != nil { + fmt.Printf("Cannot lookup source account") + err = accountErr + return + } + if len(account.AppParams) >= int(cfg.NumApp) { + break + } + time.Sleep(time.Second) + } + + appParams = account.AppParams + return +} + func takeTopAccounts(allAccounts map[string]uint64, numAccounts uint32, srcAccount string) (accounts map[string]uint64) { allAddrs := make([]string, len(allAccounts)) var i int diff --git a/shared/pingpong/config.go b/shared/pingpong/config.go index 7b9366fc81..46f5d83b40 100644 --- a/shared/pingpong/config.go +++ b/shared/pingpong/config.go @@ -52,6 +52,8 @@ type PpConfig struct { GroupSize uint32 NumAsset uint32 MinAccountAsset uint64 + NumApp uint32 + AppProgOps uint32 } // DefaultConfig object for Ping Pong diff --git a/shared/pingpong/pingpong.go b/shared/pingpong/pingpong.go index fb1cefef44..3f9fbcec36 100644 --- a/shared/pingpong/pingpong.go +++ b/shared/pingpong/pingpong.go @@ -32,7 +32,7 @@ import ( ) // PrepareAccounts to set up accounts and asset accounts required for Ping Pong run -func PrepareAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]uint64, assetParams map[uint64]v1.AssetParams, cfg PpConfig, err error) { +func PrepareAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]uint64, assetParams map[uint64]v1.AssetParams, appParams map[uint64]v1.AppParams, cfg PpConfig, err error) { cfg = initCfg accounts, cfg, err = ensureAccounts(ac, cfg) if err != nil { @@ -41,7 +41,6 @@ func PrepareAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]u } if cfg.NumAsset > 0 { - // zero out max amount for asset transactions cfg.MaxAmt = 0 @@ -84,6 +83,11 @@ func PrepareAccounts(ac libgoal.Client, initCfg PpConfig) (accounts map[string]u for k := range assetAccounts { accounts[k] = assetAccounts[k] } + } else if cfg.NumApp > 0 { + appParams, err = prepareApps(accounts, ac, cfg) + if err != nil { + return + } } for addr := range accounts { @@ -194,7 +198,7 @@ func listSufficientAccounts(accounts map[string]uint64, minimumAmount uint64, ex } // RunPingPong starts ping pong process -func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uint64, assetParam map[uint64]v1.AssetParams, cfg PpConfig) { +func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uint64, assetParam map[uint64]v1.AssetParams, appParam map[uint64]v1.AppParams, cfg PpConfig) { // Infinite loop given: // - accounts -> map of accounts to include in transfers (including src account, which we don't want to use) // - cfg -> configuration for how to proceed @@ -234,7 +238,7 @@ func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uin fromList := listSufficientAccounts(accounts, cfg.MinAccountFunds+(cfg.MaxAmt+cfg.MaxFee)*2, cfg.SrcAccount) toList := listSufficientAccounts(accounts, 0, cfg.SrcAccount) - sent, succeded, err := sendFromTo(fromList, toList, accounts, assetParam, ac, cfg) + sent, succeded, err := sendFromTo(fromList, toList, accounts, assetParam, appParam, ac, cfg) totalSent += sent totalSucceeded += succeded if err != nil { @@ -262,7 +266,7 @@ func RunPingPong(ctx context.Context, ac libgoal.Client, accounts map[string]uin } } -func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetParams map[uint64]v1.AssetParams, client libgoal.Client, cfg PpConfig) (sentCount, successCount uint64, err error) { +func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetParams map[uint64]v1.AssetParams, appParams map[uint64]v1.AppParams, client libgoal.Client, cfg PpConfig) (sentCount, successCount uint64, err error) { amt := cfg.MaxAmt fee := cfg.MaxFee for i, from := range fromList { @@ -288,24 +292,36 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara if cfg.NumAsset > 0 { amt = 1 } + + // app or asset ID + var aidx uint64 if cfg.GroupSize == 1 { - var assetID uint64 if cfg.NumAsset > 0 { // generate random assetID if we send asset txns rindex := rand.Intn(len(assetParams)) i := 0 for k := range assetParams { if i == rindex { - assetID = k + aidx = k + break + } + i++ + } + } else if cfg.NumApp > 0 { + rindex := rand.Intn(len(appParams)) + i := 0 + for k := range appParams { + if i == rindex { + aidx = k break } i++ } } else { - assetID = 0 + aidx = 0 } // Construct single txn - txn, consErr := constructTxn(from, to, fee, amt, assetID, client, cfg) + txn, consErr := constructTxn(from, to, fee, amt, aidx, client, cfg) if consErr != nil { err = consErr _, _ = fmt.Fprintf(os.Stderr, "constructTxn failed: %v\n", err) @@ -402,7 +418,7 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara accounts[to] = uint64(toBalanceChange + int64(accounts[to])) } if sendErr != nil { - _, _ = fmt.Fprintf(os.Stderr, "error sending Transaction, sleeping .5 seconds: %+v\n", sendErr) + _, _ = fmt.Fprintf(os.Stderr, "error sending Transaction, sleeping .5 seconds: %v\n", sendErr) err = sendErr time.Sleep(500 * time.Millisecond) return @@ -414,7 +430,7 @@ func sendFromTo(fromList, toList []string, accounts map[string]uint64, assetPara return } -func constructTxn(from, to string, fee, amt, assetID uint64, client libgoal.Client, cfg PpConfig) (txn transactions.Transaction, err error) { +func constructTxn(from, to string, fee, amt, aidx uint64, client libgoal.Client, cfg PpConfig) (txn transactions.Transaction, err error) { var noteField []byte const pingpongTag = "pingpong" const tagLen = uint32(len(pingpongTag)) @@ -435,13 +451,19 @@ func constructTxn(from, to string, fee, amt, assetID uint64, client libgoal.Clie crypto.RandBytes(lease[:]) } - if cfg.NumAsset == 0 { // Construct payment transaction - txn, err = client.ConstructPayment(from, to, fee, amt, noteField[:], "", lease, 0, 0) + if cfg.NumApp > 0 { // Construct app transaction + txn, err = client.MakeUnsignedAppNoOpTx(aidx, nil, nil, nil) + if err != nil { + return + } + txn.Note = noteField[:] + txn.Lease = lease + txn, err = client.FillUnsignedTxTemplate(from, 0, 0, cfg.MaxFee, txn) if !cfg.Quiet { - _, _ = fmt.Fprintf(os.Stdout, "Sending %d : %s -> %s\n", amt, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Calling app %d : %s\n", aidx, from) } - } else { // Construct asset transaction - txn, err = client.MakeUnsignedAssetSendTx(assetID, amt, to, "", "") + } else if cfg.NumAsset > 0 { // Construct asset transaction + txn, err = client.MakeUnsignedAssetSendTx(aidx, amt, to, "", "") if err != nil { _, _ = fmt.Fprintf(os.Stdout, "error making unsigned asset send tx %v\n", err) return @@ -450,10 +472,15 @@ func constructTxn(from, to string, fee, amt, assetID uint64, client libgoal.Clie txn.Lease = lease txn, err = client.FillUnsignedTxTemplate(from, 0, 0, cfg.MaxFee, txn) if !cfg.Quiet { - _, _ = fmt.Fprintf(os.Stdout, "Sending %d asset %d: %s -> %s\n", amt, assetID, from, to) + _, _ = fmt.Fprintf(os.Stdout, "Sending %d asset %d: %s -> %s\n", amt, aidx, from, to) + } + } else { + txn, err = client.ConstructPayment(from, to, fee, amt, noteField[:], "", lease, 0, 0) + if !cfg.Quiet { + _, _ = fmt.Fprintf(os.Stdout, "Sending %d : %s -> %s\n", amt, from, to) } - } + if err != nil { _, _ = fmt.Fprintf(os.Stdout, "error constructing transaction %v\n", err) return diff --git a/test/commandandcontrol/cc_agent/component/pingPongComponent.go b/test/commandandcontrol/cc_agent/component/pingPongComponent.go index c7c8098b86..c195bb5036 100644 --- a/test/commandandcontrol/cc_agent/component/pingPongComponent.go +++ b/test/commandandcontrol/cc_agent/component/pingPongComponent.go @@ -123,11 +123,12 @@ func (componentInstance *PingPongComponentInstance) startPingPong(cfg *pingpong. var accounts map[string]uint64 var assetParams map[uint64]v1.AssetParams + var appParams map[uint64]v1.AppParams var resultCfg pingpong.PpConfig // Initialize accounts if necessary, this may take several attempts while previous transactions to settle for i := 0; i < 10; i++ { - accounts, assetParams, resultCfg, err = pingpong.PrepareAccounts(ac, *cfg) + accounts, assetParams, appParams, resultCfg, err = pingpong.PrepareAccounts(ac, *cfg) if err == nil { break } else { @@ -146,7 +147,7 @@ func (componentInstance *PingPongComponentInstance) startPingPong(cfg *pingpong. componentInstance.ctx, componentInstance.cancelFunc = context.WithCancel(context.Background()) // Kick off the real processing - go pingpong.RunPingPong(componentInstance.ctx, ac, accounts, assetParams, resultCfg) + go pingpong.RunPingPong(componentInstance.ctx, ac, accounts, assetParams, appParams, resultCfg) return } diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp new file mode 100644 index 0000000000..abb028096b --- /dev/null +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -0,0 +1,98 @@ +#!/usr/bin/expect -f +#exp_internal 1 +set err 0 +log_user 1 + +proc TestGoalDryrun { DRREQ_FILE TEST_PRIMARY_NODE_DIR } { + set PASSED 0 + set PROGRAM_TYPE "" + spawn goal clerk dryrun-remote -d $TEST_PRIMARY_NODE_DIR -D $DRREQ_FILE -v + expect { + timeout { ::AlgorandGoal::Abort "goal clerk dryrun-remote timeout" } + -re {(ApprovalProgram)} {puts "match1"; set PROGRAM_TYPE $expect_out(1,string); exp_continue} + "PASS" {puts "match2"; set PASSED 1; close} + } + if { $PASSED == 0 } { + ::AlgorandGoal::Abort "Program did not pass" + } + if { $PROGRAM_TYPE != "ApprovalProgram" } { + puts "Program type: $PROGRAM_TYPE" + ::AlgorandGoal::Abort "Invalid program type" + } +} + +if { [catch { + + source goalExpectCommon.exp + set TEST_ALGO_DIR [lindex $argv 0] + set TEST_DATA_DIR [lindex $argv 1] + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Update the Primary Node configuration + exec -- cat "$TEST_ROOT_DIR/Primary/config.json" | jq {. |= . + {"EnableDeveloperAPI":true}} > $TEST_ROOT_DIR/Primary/config.json.new + exec rm $TEST_ROOT_DIR/Primary/config.json + exec mv $TEST_ROOT_DIR/Primary/config.json.new $TEST_ROOT_DIR/Primary/config.json + + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] + puts "Primary Node Address: $PRIMARY_NODE_ADDRESS" + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + # Determine primary account + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Check the balance of the primary account + set PRIMARY_ACCOUNT_BALANCE [::AlgorandGoal::GetAccountBalance $PRIMARY_WALLET_NAME $PRIMARY_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR] + puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" + + set TEAL_PROG_FILE "$TEST_ROOT_DIR/trivial.teal" + exec echo int 1 > $TEAL_PROG_FILE + + # no format parameter + set DRREQ_FILE_1 "$TEST_ROOT_DIR/app-create-drreq-1.json" + spawn goal app create --creator $PRIMARY_ACCOUNT_ADDRESS --approval-prog $TEAL_PROG_FILE --clear-prog $TEAL_PROG_FILE --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 -d $TEST_PRIMARY_NODE_DIR -o $DRREQ_FILE_1 --dryrun-dump + expect { + timeout { ::AlgorandGoal::Abort "goal app create timeout" } + } + + # explicit json + set DRREQ_FILE_2 "$TEST_ROOT_DIR/app-create-drreq-2.json" + spawn goal app create --creator $PRIMARY_ACCOUNT_ADDRESS --approval-prog $TEAL_PROG_FILE --clear-prog $TEAL_PROG_FILE --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 -d $TEST_PRIMARY_NODE_DIR -o $DRREQ_FILE_2 --dryrun-dump --dryrun-dump-format=json + expect { + timeout { ::AlgorandGoal::Abort "goal app create timeout" } + } + + # explicit msgp + set DRREQ_FILE_3 "$TEST_ROOT_DIR/app-create-drreq.msgp" + spawn goal app create --creator $PRIMARY_ACCOUNT_ADDRESS --approval-prog $TEAL_PROG_FILE --clear-prog $TEAL_PROG_FILE --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 -d $TEST_PRIMARY_NODE_DIR -o $DRREQ_FILE_3 --dryrun-dump --dryrun-dump-format=msgp + expect { + timeout { ::AlgorandGoal::Abort "goal app create timeout" } + } + + TestGoalDryrun $DRREQ_FILE_1 $TEST_PRIMARY_NODE_DIR + TestGoalDryrun $DRREQ_FILE_2 $TEST_PRIMARY_NODE_DIR + TestGoalDryrun $DRREQ_FILE_3 $TEST_PRIMARY_NODE_DIR + + # Shutdown the network + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in goalDryrunRestTest: $EXCEPTION" +} diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index ed98c2a260..f720d817bd 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -173,7 +173,7 @@ proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR spawn goal network start -r $TEST_ROOT_DIR expect { timeout { close; ::AlgorandGoal::Abort "Timed out starting network" } - ".*Network started under.* { puts "Network $NETWORK_NAME started" ;close } + ".*Network started under.*" { puts "Network $NETWORK_NAME started" ;close } close } } EXCEPTION ] } { @@ -187,7 +187,7 @@ proc ::AlgorandGoal::StartNetwork { NETWORK_NAME NETWORK_TEMPLATE TEST_ALGO_DIR spawn goal network status -r $TEST_ROOT_DIR expect { timeout { close; ::AlgorandGoal::Abort "Timed out retrieving network status" } - ".*Error getting status.*" { close; ::AlgorandGoal::Abort "error getting network status: $expect_out(buffer)""} + ".*Error getting status.*" { close; ::AlgorandGoal::Abort "error getting network status: $expect_out(buffer)"} "^Network Started under.*" { puts "Network $NETWORK_NAME status ok"; close } close } diff --git a/test/e2e-go/features/transactions/accountv2_test.go b/test/e2e-go/features/transactions/accountv2_test.go new file mode 100644 index 0000000000..695d43c3fe --- /dev/null +++ b/test/e2e-go/features/transactions/accountv2_test.go @@ -0,0 +1,216 @@ +// 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 . + +package transactions + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/framework/fixtures" +) + +func TestAccountInformationV2(t *testing.T) { + t.Parallel() + a := require.New(t) + + var fixture fixtures.RestClientFixture + proto, ok := config.Consensus[protocol.ConsensusFuture] + a.True(ok) + os.Setenv("ALGOSMALLLAMBDAMSEC", "200") + fixture.SetConsensus(config.ConsensusProtocols{protocol.ConsensusFuture: proto}) + defer func() { + os.Unsetenv("ALGOSMALLLAMBDAMSEC") + }() + + fixture.Setup(t, filepath.Join("nettemplates", "TwoNodes50EachFuture.json")) + defer fixture.Shutdown() + + client := fixture.LibGoalClient + accountList, err := fixture.GetWalletsSortedByBalance() + a.NoError(err) + + creator := accountList[0].Address + wh, err := client.GetUnencryptedWalletHandle() + a.NoError(err) + + user, err := client.GenerateAddress(wh) + a.NoError(err) + + fee := uint64(1000) + + round, err := client.CurrentRound() + a.NoError(err) + + // Fund the manager, so it can issue transactions later on + _, err = client.SendPaymentFromUnencryptedWallet(creator, user, fee, 10000000000, nil) + a.NoError(err) + client.WaitForRound(round + 2) + + // There should be no apps to start with + ad, err := client.AccountData(creator) + a.NoError(err) + a.Zero(len(ad.AppParams)) + + ad, err = client.AccountData(user) + a.NoError(err) + a.Zero(len(ad.AppParams)) + a.Equal(basics.MicroAlgos{Raw: 10000000000}, ad.MicroAlgos) + + counter := `#pragma version 2 +// a simple global and local calls counter app +byte b64 Y291bnRlcg== // counter +dup +app_global_get +int 1 ++ +app_global_put // update the counter +int 0 +int 0 +app_opted_in +bnz opted_in +err +opted_in: +int 0 // account idx for app_local_put +byte b64 Y291bnRlcg== // counter +int 0 +byte b64 Y291bnRlcg== +app_local_get +int 1 // increment ++ +app_local_put +int 1 +` + approval, err := logic.AssembleString(counter) + a.NoError(err) + clearstate, err := logic.AssembleString("int 1") + a.NoError(err) + schema := basics.StateSchema{ + NumUint: 1, + } + + // create the app + tx, err := client.MakeUnsignedAppCreateTx( + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + ) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) + a.NoError(err) + signedTxn, err := client.SignTransactionWithWallet(wh, nil, tx) + a.NoError(err) + round, err = client.CurrentRound() + a.NoError(err) + _, err = client.BroadcastTransaction(signedTxn) + a.NoError(err) + client.WaitForRound(round + 2) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + a.NoError(err) + a.Equal(1, len(ad.AppParams)) + var appIdx basics.AppIndex + var params basics.AppParams + for i, p := range ad.AppParams { + appIdx = i + params = p + break + } + a.Equal(approval, params.ApprovalProgram) + a.Equal(clearstate, params.ClearStateProgram) + a.Equal(schema, params.LocalStateSchema) + a.Equal(schema, params.GlobalStateSchema) + a.Equal(1, len(params.GlobalState)) + value, ok := params.GlobalState["counter"] + a.True(ok) + a.Equal(uint64(1), value.Uint) + + a.Equal(1, len(ad.AppLocalStates)) + state, ok := ad.AppLocalStates[appIdx] + a.True(ok) + a.Equal(schema, state.Schema) + a.Equal(1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + a.True(ok) + a.Equal(uint64(1), value.Uint) + + // call the app + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + a.NoError(err) + tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) + a.NoError(err) + signedTxn, err = client.SignTransactionWithWallet(wh, nil, tx) + a.NoError(err) + round, err = client.CurrentRound() + a.NoError(err) + _, err = client.BroadcastTransaction(signedTxn) + a.NoError(err) + client.WaitForRound(round + 2) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + a.NoError(err) + a.Equal(1, len(ad.AppParams)) + params, ok = ad.AppParams[appIdx] + a.True(ok) + a.Equal(approval, params.ApprovalProgram) + a.Equal(clearstate, params.ClearStateProgram) + a.Equal(schema, params.LocalStateSchema) + a.Equal(schema, params.GlobalStateSchema) + a.Equal(1, len(params.GlobalState)) + value, ok = params.GlobalState["counter"] + a.True(ok) + a.Equal(uint64(2), value.Uint) + + a.Equal(1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + a.True(ok) + a.Equal(schema, state.Schema) + a.Equal(1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + a.True(ok) + a.Equal(uint64(1), value.Uint) + + a.Equal(uint64(2), ad.TotalAppSchema.NumUint) + + // check user's balance record for the app entry and the state changes + ad, err = client.AccountData(user) + a.NoError(err) + a.Equal(0, len(ad.AppParams)) + + a.Equal(1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + a.True(ok) + a.Equal(schema, state.Schema) + a.Equal(1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + a.True(ok) + a.Equal(uint64(1), value.Uint) + + a.Equal(basics.MicroAlgos{Raw: 10000000000 - fee}, ad.MicroAlgos) + + app, err := client.ApplicationInformation(uint64(appIdx)) + a.NoError(err) + a.Equal(uint64(appIdx), app.Id) + a.Equal(creator, app.Params.Creator) +} diff --git a/test/e2e-go/features/transactions/asset_test.go b/test/e2e-go/features/transactions/asset_test.go index 68a2659e52..d9b1f8ea32 100644 --- a/test/e2e-go/features/transactions/asset_test.go +++ b/test/e2e-go/features/transactions/asset_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" + v1 "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/libgoal" @@ -454,6 +454,12 @@ func TestAssetInformation(t *testing.T) { a.NoError(err) a.Equal(len(info.AssetParams), 0) + // There should be no assets to start with + info2, err := client.AccountInformationV2(account0) + a.NoError(err) + a.NotNil(info2.CreatedAssets) + a.Equal(len(*info2.CreatedAssets), 0) + // Create some assets txids := make(map[string]string) for i := 0; i < 16; i++ { @@ -476,6 +482,16 @@ func TestAssetInformation(t *testing.T) { a.Equal(cp, assetInfo) } + // Check that AssetInformationV2 returns the correct AssetParams + info2, err = client.AccountInformationV2(account0) + a.NoError(err) + a.NotNil(info2.CreatedAssets) + for _, cp := range *info2.CreatedAssets { + asset, err := client.AssetInformationV2(cp.Index) + a.NoError(err) + a.Equal(cp, asset) + } + // Destroy assets txids = make(map[string]string) for idx := range info.AssetParams { diff --git a/test/scripts/e2e_subs/assets-app.sh b/test/scripts/e2e_subs/assets-app.sh new file mode 100755 index 0000000000..8288ec621b --- /dev/null +++ b/test/scripts/e2e_subs/assets-app.sh @@ -0,0 +1,282 @@ +#!/usr/bin/env bash +# TIMEOUT=300 + +date '+assets-app start %Y%m%d_%H%M%S' + +set -ex +set -o pipefail +export SHELLOPTS + +WALLET=$1 +gcmd="goal -w ${WALLET}" + +# Directory of helper TEAL programs +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )/tealprogs" + +CREATOR=$(${gcmd} account list|awk '{ print $3 }') +ALICE=$(${gcmd} account new|awk '{ print $6 }') +BOB=$(${gcmd} account new|awk '{ print $6 }') +MANAGER=$(${gcmd} account new|awk '{ print $6 }') + +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${ALICE} +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${BOB} +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${MANAGER} + +ZERO='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ' +SUPPLY=10000000 +XFER1=1000 +XFER2=42 +XFER3=99999 +XFER4=11 + +APP_CREATED_STR='Created app with app index' +ERR_APP_CL_STR='only clearing out is supported for applications that do not exist' +ERR_APP_NE_STR='application does not exist' +ERR_APP_OI_STR1='has not opted in to application' +ERR_APP_OI_STR2='not opted in to app' +ERR_APP_OI_STR3='is not currently opted in' +ERR_APP_REJ_STR1='transaction rejected by ApprovalProgram' +ERR_APP_REJ_STR2='TEAL runtime encountered err opcode' +ERR_APP_REJ_STR3='- would result negative' + +### Basic reading, creation, deletion, transfers, and freezing + +# create +APP_ID=$(${gcmd} app interact execute --header ${DIR}/asa.json --from $CREATOR --approval-prog ${DIR}/asa_approve.teal --clear-prog ${DIR}/asa_clear.teal create --manager $CREATOR --reserve $CREATOR --freezer $CREATOR --clawback $CREATOR --supply $SUPPLY | grep "$APP_CREATED_STR" | cut -d ' ' -f 6) + +qcmd="${gcmd} app interact query --header ${DIR}/asa.json --app-id $APP_ID" +xcmd="${gcmd} app interact execute --header ${DIR}/asa.json --app-id $APP_ID" + +# read global +RES=$(${qcmd} total-supply) +if [[ $RES != $SUPPLY ]]; then + date "+assets-app FAIL expected supply to be set to $SUPPLY %Y%m%d_%H%M%S" + false +fi + +RES=$(${qcmd} creator-balance) +if [[ $RES != $SUPPLY ]]; then + date "+assets-app FAIL expected creator to begin with $SUPPLY %Y%m%d_%H%M%S" + false +fi + +# read alice F +RES=$(${qcmd} --from $ALICE balance 2>&1 || true) +if [[ $RES != *"$ERR_APP_OI_STR1"* ]]; then + date '+assets-app FAIL expected read of non-opted in account to fail %Y%m%d_%H%M%S' + false +fi + +# optin alice +${xcmd} --from $ALICE opt-in + +# read alice +RES=$(${qcmd} --from $ALICE balance) +if [[ $RES != '0' ]]; then + date '+assets-app FAIL expected opted-in account to start with no balance %Y%m%d_%H%M%S' + false +fi + +RES=$(${qcmd} --from $ALICE frozen) +if [[ $RES != '0' ]]; then + date '+assets-app FAIL expected opted-in account to be non-frozen %Y%m%d_%H%M%S' + false +fi + +# xfer0 creator -> bob F +RES=$(${xcmd} --from $CREATOR transfer --receiver $BOB --amount $XFER1 2>&1 || true) +if [[ $RES != *"$ERR_APP_OI_STR2"* ]]; then + date '+assets-app FAIL transfer succeeded on account which has not opted in %Y%m%d_%H%M%S' + false +fi + +# xfer1 (2) creator -> alice +${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 +${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 + +# read alice +RES=$(${qcmd} --from $ALICE balance) +if [[ $RES != $(( $XFER1 + $XFER1 )) ]]; then + date "+assets-app FAIL transfer recipient does not have $XFER1 %Y%m%d_%H%M%S" + false +fi + +# destroy F +RES=$(${xcmd} --from $CREATOR destroy 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date "+assets-app FAIL should not be able to destroy asset while outstanding holdings exist %Y%m%d_%H%M%S" + false +fi + +# freeze +${xcmd} --from $CREATOR freeze --frozen 1 --target $ALICE + +# xfer2 alice -> creator F +RES=$(${xcmd} --from $ALICE transfer --receiver $CREATOR --amount $XFER2 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR2"* ]]; then + date "+assets-app FAIL frozen account should not be able to send %Y%m%d_%H%M%S" + false +fi + +# xfer1 creator -> alice F +RES=$(${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR2"* ]]; then + date "+assets-app FAIL frozen account should not be able to receive %Y%m%d_%H%M%S" + false +fi + +# unfreeze +${xcmd} --from $CREATOR freeze --frozen 0 --target $ALICE + +# xfer1 creator -> alice +${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 + +# xfer5 alice |-> alice F +RES=$(${xcmd} --from $ALICE close-out --close-to $ALICE 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date "+assets-app FAIL closing to self not permitted %Y%m%d_%H%M%S" + false +fi + +# optin bob +${xcmd} --from $BOB opt-in + +# xfer3 alice -> bob overdraw F +RES=$(${xcmd} --from $ALICE transfer --receiver $BOB --amount $XFER3 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR3"* ]]; then + date "+assets-app FAIL overdraws are not permitted %Y%m%d_%H%M%S" + false +fi + +# xfer4 alice -> creator |-> bob +${xcmd} --from $ALICE close-out --receiver $CREATOR --amount $XFER4 --close-to $BOB + +# xfer5 bob |-> alice F +RES=$(${xcmd} --from $BOB close-out --close-to $ALICE 2>&1 || true) +if [[ $RES != *"$ERR_APP_OI_STR2"* ]]; then + date "+assets-app FAIL transfer succeeded on account which has closed out %Y%m%d_%H%M%S" + false +fi + +# optin alice +${xcmd} --from $ALICE opt-in + +# xfer5 bob |-> alice +${xcmd} --from $BOB close-out --close-to $ALICE + +# clear alice +${xcmd} --from $ALICE clear + +# clear alice F +RES=$(${xcmd} --from $ALICE clear 2>&1 || true) +if [[ $RES != *"$ERR_APP_OI_STR3"* ]]; then + date "+assets-app FAIL should not be able to clear asset holding twice %Y%m%d_%H%M%S" + false +fi + +# destroy +${xcmd} --from $CREATOR destroy + +# destroy F +RES=$(${xcmd} --from $CREATOR destroy 2>&1 || true) +if [[ $RES != *"$ERR_APP_CL_STR"* ]]; then + date '+assets-app FAIL second deletion of application should fail %Y%m%d_%H%M%S' + false +fi + +# optin alice F +RES=$(${xcmd} --from $ALICE opt-in 2>&1 || true) +if [[ $RES != *"$ERR_APP_CL_STR"* ]]; then + date '+assets-app FAIL optin of deleted application should fail %Y%m%d_%H%M%S' + false +fi + +# read global F +RES=$(${qcmd} total-supply 2>&1 || true) +if [[ $RES != *"$ERR_APP_NE_STR"* ]]; then + date '+assets-app FAIL read global of deleted application should fail %Y%m%d_%H%M%S' + false +fi + +### Reconfiguration, default-frozen, and clawback + +# create frozen +APP_ID=$(${gcmd} app interact execute --header ${DIR}/asa.json --from $CREATOR --approval-prog ${DIR}/asa_approve.teal --clear-prog ${DIR}/asa_clear.teal create --manager $MANAGER --reserve $CREATOR --freezer $MANAGER --clawback $MANAGER --supply $SUPPLY --default-frozen 1 | grep "$APP_CREATED_STR" | cut -d ' ' -f 6) + +qcmd="${gcmd} app interact query --header ${DIR}/asa.json --app-id $APP_ID" +xcmd="${gcmd} app interact execute --header ${DIR}/asa.json --app-id $APP_ID" + +# destroy bad manager F +RES=$(${xcmd} --from $CREATOR destroy 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date "+assets-app FAIL non-manager should not be able to delete asset %Y%m%d_%H%M%S" + false +fi + +# optin alice +${xcmd} --from $ALICE opt-in + +# xfer1 F +RES=$(${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR2"* ]]; then + date "+assets-app FAIL frozen account should not be able to receive %Y%m%d_%H%M%S" + false +fi + +# bad unfreeze F +RES=$(${xcmd} --from $ALICE freeze --frozen 0 --target $ALICE 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date "+assets-app FAIL non-freezer should not be able to unfreeze account %Y%m%d_%H%M%S" + false +fi + +# set freezer alice +${xcmd} --from $MANAGER reconfigure --manager $MANAGER --reserve $CREATOR --freezer $ALICE --clawback $MANAGER + +# unfreeze +${xcmd} --from $ALICE freeze --frozen 0 --target $ALICE + +# xfer1 +${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 + +# freeze +${xcmd} --from $ALICE freeze --frozen 1 --target $ALICE + +# xfer1 F +RES=$(${xcmd} --from $CREATOR transfer --receiver $ALICE --amount $XFER1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR2"* ]]; then + date "+assets-app FAIL re-frozen account should not be able to receive %Y%m%d_%H%M%S" + false +fi + +# closeout F +RES=$(${xcmd} --from $ALICE close-out --close-to $CREATOR 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR2"* ]]; then + date "+assets-app FAIL frozen account should not be able to closeout w/o clear %Y%m%d_%H%M%S" + false +fi + +# clear alice +${xcmd} --from $ALICE clear + +# optin bob +${xcmd} --from $BOB opt-in + +# clawback transfer +${xcmd} --from $MANAGER clawback --sender $CREATOR --receiver $BOB --amount $XFER1 + +# destroy F +RES=$(${xcmd} --from $MANAGER destroy 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date "+assets-app FAIL should not be able to delete asset while outstanding holdings exist %Y%m%d_%H%M%S" + false +fi + +# clawback +${xcmd} --from $MANAGER clawback --sender $BOB --receiver $CREATOR --amount $XFER1 + +# destroy +${xcmd} --from $MANAGER destroy + +# clear bob +${xcmd} --from $BOB clear diff --git a/test/scripts/e2e_subs/e2e-app-bootloader.sh b/test/scripts/e2e_subs/e2e-app-bootloader.sh new file mode 100755 index 0000000000..9019bbc8b8 --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-bootloader.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Compile target contract (we will upgrade ApprovalProgram and +# ClearStateProgram into this) +${gcmd} clerk compile ${DIR}/tealprogs/upgraded.teal -o ${TEMPDIR}/upgraded.tealc +TARGET_HASH=$(shasum -a 256 ${TEMPDIR}/upgraded.tealc | awk '{ print $1 }') + +# Compile dummy, wrong contract +${gcmd} clerk compile ${DIR}/tealprogs/wrongupgrade.teal -o ${TEMPDIR}/wrongupgrade.tealc + +# Copy template +cp ${DIR}/tealprogs/bootloader.teal.tmpl ${TEMPDIR}/bootloader.teal + +# Substitute template values +sed -i"" -e "s/TMPL_APPROV_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal +sed -i"" -e "s/TMPL_CLEARSTATE_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal + +# Create an app using filled-in bootloader template +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${TEMPDIR}/bootloader.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') + +# Calling app without args and wrong OnCompletion should fail +EXPERROR='rejected by ApprovalProgram' +RES=$(${gcmd} app call --app-id $APPID --from $ACCOUNT 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-bootloader-test FAIL call with no progs should fail %Y%m%d_%H%M%S' + false +fi + +# Calling app as an update but with wrong scripts should fail +EXPERROR='rejected by ApprovalProgram' +RES=$(${gcmd} app update --app-id $APPID --from $ACCOUNT --approval-prog-raw ${TEMPDIR}/wrongupgrade.tealc --clear-prog-raw ${TEMPDIR}/wrongupgrade.tealc 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-bootloader-test FAIL update call with wrong progs should fail %Y%m%d_%H%M%S' + false +fi + +# Calling app as an update but with right scripts should succeed +${gcmd} app update --app-id $APPID --from $ACCOUNT --approval-prog-raw ${TEMPDIR}/upgraded.tealc --clear-prog-raw ${TEMPDIR}/upgraded.tealc + +# Global state should be empty +RES=$(${gcmd} app read --guess-format --app-id $APPID --global | jq -r .foo.tb) +if [[ "$RES" != "null" ]]; then + date '+app-bootloader-test FAIL unexpected global state after update %Y%m%d_%H%M%S' + false +fi + +# Calling app should succeed +${gcmd} app call --app-id $APPID --from $ACCOUNT + +# Global state should now have 'foo': 'foo' key +RES=$(${gcmd} app read --guess-format --app-id $APPID --global | jq -r .foo.tb) +if [[ "$RES" != "foo" ]]; then + date '+app-bootloader-test FAIL unexpected global state after update and call %Y%m%d_%H%M%S' + false +fi diff --git a/test/scripts/e2e_subs/e2e-app-closeout.sh b/test/scripts/e2e_subs/e2e-app-closeout.sh new file mode 100755 index 0000000000..c116c3a755 --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-closeout.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +date '+app-closeout start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') +ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') + +# Fund ACCOUNTB +${gcmd} clerk send -a 100000000 -f ${ACCOUNT} -t ${ACCOUNTB} + +# Create an application that uses some global/local state +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 1') --global-byteslices 1 --global-ints 1 --local-byteslices 1 --local-ints 1 --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') + +# Should succeed to opt in +${gcmd} app optin --app-id $APPID --from $ACCOUNTB + +# Closing out the account should fail +EXPERROR='outstanding applications' +RES=$(${gcmd} clerk send --from $ACCOUNTB --close-to $ACCOUNT --to $ACCOUNT -a 0 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-closeout FAIL closing out account should fail if still opted in to app %Y%m%d_%H%M%S' + false +fi + +# Closing out of app should succeed +${gcmd} app closeout --app-id $APPID --from $ACCOUNTB + +# Closing out the account should now succeed +${gcmd} clerk send --from $ACCOUNTB --close-to $ACCOUNT --to $ACCOUNT -a 0 + +# Fund ACCOUNTB again +${gcmd} clerk send -a 100000000 -f ${ACCOUNT} -t ${ACCOUNTB} + +# Should succeed to opt again +${gcmd} app optin --app-id $APPID --from $ACCOUNTB + +# Clearing out of app should succeed +${gcmd} app closeout --app-id $APPID --from $ACCOUNTB + +# Closing out the account should still succeed +${gcmd} clerk send --from $ACCOUNTB --close-to $ACCOUNT --to $ACCOUNT -a 0 + +# Closing out the creator's account should fail +EXPERROR='outstanding created applications' +RES=$(${gcmd} clerk send --from $ACCOUNT --close-to $ACCOUNTB --to $ACCOUNTB -a 0 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-closeout FAIL closing out account should fail if created app still exists %Y%m%d_%H%M%S' + false +fi + +# Deleting application should succeed +${gcmd} app delete --app-id $APPID --from $ACCOUNT + +# Closing out the creator's account should now succeed +${gcmd} clerk send --from $ACCOUNT --close-to $ACCOUNTB --to $ACCOUNTB -a 0 diff --git a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh new file mode 100755 index 0000000000..ac6841b135 --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Create an ASA in account +${gcmd} asset create --creator ${ACCOUNT} --name bogocoin --unitname bogo --total 1337 +ASSET_ID=$(${gcmd} asset info --creator $ACCOUNT --asset bogo|grep 'Asset ID'|awk '{ print $3 }') + +# Create app that reads asset balance and checks asset details and checks round +ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') +TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) +APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo "int 1") | grep Created | awk '{ print $6 }') + +# Create another account, fund it, send it some asset +ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') +${gcmd} clerk send -a 1000000 -f $ACCOUNT -t $ACCOUNTB +${gcmd} asset send --assetid $ASSET_ID -a 0 -f $ACCOUNTB -t $ACCOUNTB +${gcmd} asset send --assetid $ASSET_ID -a 17 -f $ACCOUNT -t $ACCOUNTB + +# Call app from account B, do some checks on asset balance +ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') +TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) +${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:0" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" + +# Freeze account B's holding +${gcmd} asset freeze --assetid $ASSET_ID --freeze=true --freezer $ACCOUNT --account $ACCOUNTB + +# Check bit flipped +ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') +TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) +${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:1" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" diff --git a/test/scripts/e2e_subs/e2e-app-simple.sh b/test/scripts/e2e_subs/e2e-app-simple.sh new file mode 100755 index 0000000000..adb96f4064 --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-simple.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Succeed in creating app that approves all transactions +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 1') --clear-prog <(echo 'int 1') --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 | grep Created | awk '{ print $6 }') + +# Fail to create app if approval program rejects creation +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 0') --clear-prog <(echo 'int 1') --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) +EXPERROR='rejected by ApprovalProgram' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL txn with failing approval prog should be rejected %Y%m%d_%H%M%S' + false +fi + +# Succeed in opting into the first app +${gcmd} app optin --app-id $APPID --from $ACCOUNT + +# Succeed in closing out of the first app +${gcmd} app closeout --app-id $APPID --from $ACCOUNT + +# Fail to close out twice +RES=$(${gcmd} app closeout --app-id $APPID --from $ACCOUNT 2>&1 || true) +EXPERROR='is not opted in' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL closing out twice should fail %Y%m%d_%H%M%S' + false +fi + +# Succeed in opting into the first app again +${gcmd} app optin --app-id $APPID --from $ACCOUNT + +# Succeed in clearing state for the app +${gcmd} app clear --app-id $APPID --from $ACCOUNT + +# Fail to clear twice +RES=$(${gcmd} app clear --app-id $APPID --from $ACCOUNT 2>&1 || true) +EXPERROR='not currently opted in' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL clearing state twice should fail %Y%m%d_%H%M%S' + false +fi diff --git a/test/scripts/e2e_subs/e2e-app-stateful-global.sh b/test/scripts/e2e_subs/e2e-app-stateful-global.sh new file mode 100755 index 0000000000..f09802ce12 --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-stateful-global.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Succeed in creating app that approves transactions with arg[0] == 'hello' +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globcheck.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:hello" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') + +# Application call with no args should fail +EXPERROR='invalid ApplicationArgs index 0' +RES=$(${gcmd} app call --app-id $APPID --from $ACCOUNT 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL call with no args should fail %Y%m%d_%H%M%S' + false +fi + +# Should succeed to opt in with first arg hello +${gcmd} app optin --app-id $APPID --from $ACCOUNT --app-arg "str:hello" + +# Write should now succeed +${gcmd} app call --app-id $APPID --app-arg "str:write" --from $ACCOUNT + +# Check should now succeed with value "bar" +${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT + +# Should succeed to close out with first arg hello +${gcmd} app closeout --app-id $APPID --from $ACCOUNT --app-arg "str:hello" + +# Write/opt in in one tx should succeed +${gcmd} app optin --app-id $APPID --from $ACCOUNT --app-arg "str:write" + +# Check should still succeed +${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT + +# Delete application should still succeed +${gcmd} app delete --app-id $APPID --app-arg "str:hello" --from $ACCOUNT + +# Check should fail since we can't find program to execute +RES=$(${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT 2>&1 || true) +EXPERROR='only clearing out is supported' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL app call should fail if app has been deleted %Y%m%d_%H%M%S' + false +fi + +# Clear should still succeed with arbitrary args +${gcmd} app clear --app-id $APPID --app-arg "str:asdf" --from $ACCOUNT diff --git a/test/scripts/e2e_subs/e2e-app-stateful-local.sh b/test/scripts/e2e_subs/e2e-app-stateful-local.sh new file mode 100755 index 0000000000..dda8f00e4b --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-stateful-local.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Succeed in creating app that approves transactions with arg[0] == 'hello' +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/loccheck.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --app-arg "str:hello" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') + +# Application call with no args should fail +EXPERROR='invalid ApplicationArgs index 0' +RES=$(${gcmd} app call --app-id $APPID --from $ACCOUNT 2>&1 || true) +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL call with no args should fail %Y%m%d_%H%M%S' + false +fi + +# Application call with arg0 == "write" should fail before we opt in +RES=$(${gcmd} app call --app-id $APPID --app-arg "str:write" --from $ACCOUNT 2>&1 || true) +EXPERROR='not opted in' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL writing state should fail if account has not opted in %Y%m%d_%H%M%S' + false +fi + +# Should succeed to opt in with first arg hello +${gcmd} app optin --app-id $APPID --from $ACCOUNT --app-arg "str:hello" + +# Write should now succeed +${gcmd} app call --app-id $APPID --app-arg "str:write" --from $ACCOUNT + +# Check should now succeed with value "bar" +${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT + +# Should succeed to close out with first arg hello +${gcmd} app closeout --app-id $APPID --from $ACCOUNT --app-arg "str:hello" + +# Write/opt in in one tx should succeed +${gcmd} app optin --app-id $APPID --from $ACCOUNT --app-arg "str:write" + +# Check should still succeed +${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT + +# Delete application should still succeed +${gcmd} app delete --app-id $APPID --app-arg "str:hello" --from $ACCOUNT + +# Check should fail since we can't find program to execute +RES=$(${gcmd} app call --app-id $APPID --app-arg "str:check" --app-arg "str:bar" --from $ACCOUNT 2>&1 || true) +EXPERROR='only clearing out is supported' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL app call should fail if app has been deleted %Y%m%d_%H%M%S' + false +fi + +# Clear should still succeed with arbitrary args +${gcmd} app clear --app-id $APPID --app-arg "str:asdf" --from $ACCOUNT diff --git a/test/scripts/e2e_subs/e2e-app-x-app-reads.sh b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh new file mode 100755 index 0000000000..4c1eb1237a --- /dev/null +++ b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +date '+keyreg-teal-test start %Y%m%d_%H%M%S' + +set -e +set -x +set -o pipefail +export SHELLOPTS + +WALLET=$1 + +# Directory of this bash program +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" + +gcmd="goal -w ${WALLET}" + +ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') + +# Create an app with global state "foo" = "xxx" +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globwrite.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:xxx" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') + +# Creating an app that attempts to read APPID's global state without setting +# foreignapps should fail +EXPERR="invalid ForeignApps index" +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') 2>&1 || true) +if [[ $RES != *"$EXPERR"* ]]; then + date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' + false +fi + +# Creating an app that attempts to read APPID's global state and compare with +# "bar" should make it past the foreign-app check, but fail since +# "xxx" != "bar" +EXPERR="rejected by ApprovalProgram" +RES=$(${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') 2>&1 || true) +if [[ $RES != *"$EXPERR"* ]]; then + date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' + false +fi + +# Update value at "foo" to be "bar" in app $APPID +${gcmd} app call --app-id $APPID --from $ACCOUNT --app-arg "str:bar" + +# Creating other app should now succeed with properly set foreignapps +AARGS="{args: [{encoding: \"int\", value: \"$APPID\"}], foreignapps: [$APPID]}" +${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') diff --git a/test/scripts/e2e_subs/e2e_teal.sh b/test/scripts/e2e_subs/e2e-teal.sh similarity index 100% rename from test/scripts/e2e_subs/e2e_teal.sh rename to test/scripts/e2e_subs/e2e-teal.sh diff --git a/test/scripts/e2e_subs/limit-swap-test.sh b/test/scripts/e2e_subs/limit-swap-test.sh index 7127a32543..52907b47ff 100755 --- a/test/scripts/e2e_subs/limit-swap-test.sh +++ b/test/scripts/e2e_subs/limit-swap-test.sh @@ -42,7 +42,6 @@ echo "closeout part b, asset trader" # quick expiration, test closeout ROUND=$(goal node status | grep 'Last committed block:'|awk '{ print $4 }') - SETUP_ROUND=$((${ROUND} + 10)) TIMEOUT_ROUND=$((${SETUP_ROUND} + 1)) diff --git a/test/scripts/e2e_subs/sectok-app.sh b/test/scripts/e2e_subs/sectok-app.sh new file mode 100755 index 0000000000..e605210704 --- /dev/null +++ b/test/scripts/e2e_subs/sectok-app.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# TIMEOUT=300 + +date '+sectok-app start %Y%m%d_%H%M%S' + +set -ex +set -o pipefail +export SHELLOPTS + +WALLET=$1 +gcmd="goal -w ${WALLET}" + +# Directory of helper TEAL programs +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )/tealprogs" + +CREATOR=$(${gcmd} account list|awk '{ print $3 }') +ALICE=$(${gcmd} account new|awk '{ print $6 }') +BOB=$(${gcmd} account new|awk '{ print $6 }') +CAROL=$(${gcmd} account new|awk '{ print $6 }') +# MANAGER=$(${gcmd} account new|awk '{ print $6 }') + +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${ALICE} +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${BOB} +${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${CAROL} +# ${gcmd} clerk send -a 100000000 -f ${CREATOR} -t ${MANAGER} + +ZERO='AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ' +SUPPLY=10000000 +XFER1=1000 +XFER2=42 +XFER3=99999 +XFER4=11 +VERY_LATE=9999999999 + +APP_CREATED_STR='Created app with app index' +ERR_APP_CL_STR='only clearing out is supported for applications that do not exist' +ERR_APP_NE_STR='application does not exist' +ERR_APP_OI_STR1='has not opted in to application' +ERR_APP_OI_STR2='not opted in to app' +ERR_APP_OI_STR3='is not currently opted in' +ERR_APP_REJ_STR1='transaction rejected by ApprovalProgram' +ERR_APP_REJ_STR2='TEAL runtime encountered err opcode' +ERR_APP_REJ_STR3='- would result negative' + +# create +APP_ID=$(${gcmd} app interact execute --header ${DIR}/sectok.json --from $CREATOR --approval-prog ${DIR}/sectok_approve.teal --clear-prog ${DIR}/sectok_clear.teal create --token-params '{}' --total-supply $SUPPLY | grep "$APP_CREATED_STR" | cut -d ' ' -f 6) + +xcmd="${gcmd} app interact execute --header ${DIR}/sectok.json --app-id ${APP_ID}" +qcmd="${gcmd} app interact query --header ${DIR}/sectok.json --app-id ${APP_ID}" + +# read global +RES=$(${qcmd} total-supply) +if [[ $RES != $SUPPLY ]]; then + date "+sectok-app FAIL expected supply to be set to $SUPPLY %Y%m%d_%H%M%S" + false +fi + +RES=$(${qcmd} reserve-supply) +if [[ $RES != $SUPPLY ]]; then + date "+sectok-app FAIL expected reserve to begin with $SUPPLY %Y%m%d_%H%M%S" + false +fi + +# read alice F +RES=$(${qcmd} --from $ALICE balance 2>&1 || true) +if [[ $RES != *"$ERR_APP_OI_STR1"* ]]; then + date '+sectok-app FAIL expected read of non-opted in account to fail %Y%m%d_%H%M%S' + false +fi + +# optin alice, bob, carol +${xcmd} --from $ALICE opt-in +${xcmd} --from $BOB opt-in +${xcmd} --from $CAROL opt-in + +RES=$(${qcmd} --from $ALICE transfer-group) +if [[ $RES != '0' ]]; then + date '+sectok-app FAIL expected opt-in account to start with transfer group 0 %Y%m%d_%H%M%S' + false +fi + +RES=$(${qcmd} --from $ALICE balance) +if [[ $RES != '0' ]]; then + date '+sectok-app FAIL expected opt-in account to start with 0 balance %Y%m%d_%H%M%S' + false +fi + +# assorted transfer-admin restrictions +RES=$(${xcmd} --from $CREATOR set-transfer-group --target $ALICE --transfer-group 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL contract-admins cannot set transfer groups %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $CREATOR set-lock-until --target $ALICE --lock-until 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL contract-admins cannot set lock-until %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $CREATOR set-max-balance --target $ALICE --max-balance $SUPPLY 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL contract-admins cannot set max balance %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $ALICE set-transfer-group --target $ALICE --transfer-group 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot set transfer groups %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $ALICE set-lock-until --target $ALICE --lock-until 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot set lock-until %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $ALICE set-max-balance --target $ALICE --max-balance $SUPPLY 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot set max balance %Y%m%d_%H%M%S' + false +fi + +# setting transfer-admin +RES=$(${xcmd} --from $ALICE freeze --target $ALICE --frozen 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot freeze accounts %Y%m%d_%H%M%S' + false +fi + +RES=$(${xcmd} --from $ALICE set-transfer-admin --target $ALICE --status 1 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot set transfer admin status %Y%m%d_%H%M%S' + false +fi + +${xcmd} --from $CREATOR set-transfer-admin --target $ALICE --status 1 +${xcmd} --from $ALICE freeze --target $ALICE --frozen 1 +${xcmd} --from $ALICE set-max-balance --target $ALICE --max-balance $SUPPLY +${xcmd} --from $CREATOR set-transfer-admin --target $ALICE --status 0 + +RES=$(${xcmd} --from $ALICE freeze --target $ALICE --frozen 0 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins (revoked) cannot freeze accounts %Y%m%d_%H%M%S' + false +fi + +# setting contract-admin +${xcmd} --from $CREATOR set-contract-admin --target $BOB --status 1 +${xcmd} --from $BOB set-transfer-admin --target $ALICE --status 1 +${xcmd} --from $CREATOR set-contract-admin --target $BOB --status 0 + +RES=$(${xcmd} --from $BOB set-transfer-admin --target $ALICE --status 0 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR1"* ]]; then + date '+sectok-app FAIL non-admins cannot set transfer admin status %Y%m%d_%H%M%S' + false +fi + +# minting/burning +${xcmd} --from $CREATOR mint --target $ALICE --amount $XFER1 +${xcmd} --from $CREATOR mint --target $ALICE --amount $XFER1 + +RES=$(${qcmd} --from $ALICE balance) +if [[ $RES != $(( $XFER1 + $XFER1 )) ]]; then + date '+sectok-app FAIL minting twice did not produce the correct balance %Y%m%d_%H%M% S' + false +fi + +RES=$(${qcmd} reserve-supply) +if [[ $RES != $(( $SUPPLY - $XFER1 - $XFER1 )) ]]; then + date '+sectok-app FAIL minting twice did not produce the correct reserve balance %Y%m%d_%H%M% S' + false +fi + +${xcmd} --from $CREATOR burn --target $ALICE --amount $XFER1 + +RES=$(${qcmd} --from $ALICE balance) +if [[ $RES != $XFER1 ]]; then + date '+sectok-app FAIL minting and then burning did not produce the correct balance %Y%m%d_%H%M% S' + false +fi + +${xcmd} --from $CREATOR burn --target $ALICE --amount $XFER1 + +# allowing transfers and transferring +${xcmd} --from $CREATOR mint --target $CAROL --amount $XFER1 + +RES=$(${xcmd} --from $CAROL transfer --receiver $BOB --amount $XFER2 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR3"* ]]; then + date '+sectok-app FAIL new account should not be able to spend %Y%m%d_%H%M% S' + false +fi + +${xcmd} --from $ALICE set-max-balance --target $CAROL --max-balance $SUPPLY +${xcmd} --from $ALICE set-max-balance --target $BOB --max-balance $SUPPLY +${xcmd} --from $ALICE set-lock-until --target $CAROL --lock-until 1 +${xcmd} --from $ALICE set-lock-until --target $BOB --lock-until 1 +${xcmd} --from $ALICE set-transfer-group --target $CAROL --transfer-group 1 +${xcmd} --from $ALICE set-transfer-group --target $BOB --transfer-group 2 + +RES=$(${xcmd} --from $CAROL transfer --receiver $BOB --amount $XFER2 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR3"* ]]; then + date '+sectok-app FAIL no transfers allowed without transfer rules %Y%m%d_%H%M% S' + false +fi + +${xcmd} --from $ALICE set-transfer-rule --send-group 1 --receive-group 2 --lock-until 1 +${xcmd} --from $CAROL transfer --receiver $BOB --amount $XFER2 + +RES=$(${xcmd} --from $BOB transfer --receiver $CAROL --amount $XFER2 2>&1 || true) +if [[ $RES != *"$ERR_APP_REJ_STR3"* ]]; then + date '+sectok-app FAIL reverse transfer (by group) should fail %Y%m%d_%H%M% S' + false +fi diff --git a/test/scripts/e2e_subs/tealprogs/asa.json b/test/scripts/e2e_subs/tealprogs/asa.json new file mode 100644 index 0000000000..3a4b46e530 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/asa.json @@ -0,0 +1,230 @@ +{ + "execute" : { + "create" : { + "create" : true, + "help" : "Create a new asset.", + "args" : [ + { + "name" : "manager", + "kind" : "address", + "help" : "The initial asset manager, if nonzero." + }, + { + "name" : "reserve", + "kind" : "address", + "help" : "The initial reserve address, if nonzero." + }, + { + "name" : "freezer", + "kind" : "address", + "help" : "The initial freezer address, if nonzero." + }, + { + "name" : "clawback", + "kind" : "address", + "help" : "The initial clawback address, if nonzero." + }, + { + "name" : "supply", + "kind" : "integer", + "help" : "The total asset supply." + }, + { + "name" : "default-frozen", + "kind" : "integer", + "help" : "The default frozen status for new accounts." + } + ] + }, + "destroy" : { + "on-completion" : "DeleteApplication", + "help" : "Destroy an asset. The asset creator must hold the entire supply. This transaction must be sent by the asset creator." + }, + "reconfigure" : { + "help" : "Reconfigure an asset's managers. A manager can be set only if it is currently not the zero address. This transaction must be set by the asset's manager.", + "args" : [ + { + "name" : "manager", + "kind" : "address", + "help" : "The new asset manager (or zero to disable permanently)." + }, + { + "name" : "reserve", + "kind" : "address", + "help" : "The new reserve address (or zero to disable permanently)." + }, + { + "name" : "freezer", + "kind" : "address", + "help" : "The new freezer address (or zero to disable permanently)." + }, + { + "name" : "clawback", + "kind" : "address", + "help" : "The new clawback address (or zero to disable permanently)." + }, + { + "name" : "ignored0", + "kind" : "integer", + "pseudo" : true + }, + { + "name" : "ignored1", + "kind" : "integer", + "pseudo" : true + } + ] + }, + "opt-in" : { + "on-completion" : "OptIn", + "help" : "Opt into an asset. The asset creator is always opted in." + }, + "freeze" : { + "args" : [ + { + "name" : "frozen", + "kind" : "integer", + "help" : "The new frozen value of the account." + } + ], + "accounts" : [ + { + "name" : "target", + "help" : "The target account whose frozen value to set." + } + ], + "help" : "Set the frozen value of an account. This transaction must be sent by the asset freezer." + }, + "transfer" : { + "args" : [ + { + "name" : "amount", + "kind" : "integer", + "help" : "The amount of the asset to transfer." + } + ], + "accounts" : [ + { + "name" : "receiver", + "help" : "The account receiving the assets." + }, + { + "name" : "ignored", + "pseudo" : true + } + ], + "help" : "Transfer an asset to a receiver." + }, + "clawback" : { + "args" : [ + { + "name" : "amount", + "kind" : "integer", + "help" : "The amount of the asset to transfer." + }, + { + "name" : "ignored", + "kind" : "integer", + "pseudo" : true + } + ], + "accounts" : [ + { + "name" : "sender", + "help" : "The account sending the assets." + }, + { + "name" : "receiver", + "help" : "The account receiving the assets." + } + ], + "help" : "Force the transfer of an asset from some sender to some receiver. This transaction must be sent by the asset clawback account." + }, + "close-out" : { + "on-completion" : "CloseOut", + "args" : [ + { + "name" : "amount", + "kind" : "integer", + "help" : "An optional amount of the asset to transfer to a receiver before closing out." + } + ], + "accounts" : [ + { + "name" : "receiver", + "help" : "An optional account to receive assets before closing out." + }, + { + "name" : "close-to", + "help" : "The address receiving all remaining assets." + } + ], + "help" : "Close out all assets to an account, with an optional transfer beforehand." + }, + "clear" : { + "on-completion" : "ClearState", + "help" : "Clear out all assets from the account." + } + }, + "query" : { + "global" : { + "creator" : { + "key" : "cr", + "kind" : "address", + "help" : "The asset creator created the asset. The asset creator is always opted in to the asset: its balance and freeze status are stored in global storage at \"creator-balance\" and \"creator-frozen\", respectively. This value is constant." + }, + "manager" : { + "key" : "mn", + "kind" : "address", + "help" : "The asset manager can set the manager, reserve, freezer, and clawback addresses if they are nonzero." + }, + "reserve" : { + "key" : "rv", + "kind" : "address", + "help" : "The asset reserve for the asset." + }, + "freezer" : { + "key" : "fr", + "kind" : "address", + "help" : "The asset freezer can execute the \"freeze\" procedure on accounts." + }, + "clawback" : { + "key" : "cl", + "kind" : "address", + "help" : "The asset clawback address can execute the \"clawback\" procedure on accounts." + }, + "total-supply" : { + "key" : "tt", + "kind" : "integer", + "help" : "The total supply of the asset when initially created. This value is constant." + }, + "default-frozen" : { + "key" : "df", + "kind" : "integer", + "help" : "The default frozen status of any created new account (excepting the creator, whose frozen status is initialized to 0). A value of 1 signifies that the account is frozen and cannot make ordinary transactions. This value is constant." + }, + "creator-balance" : { + "key" : "bl", + "kind" : "integer", + "help" : "The balance of the \"creator\"." + }, + "creator-frozen" : { + "key" : "fz", + "kind" : "integer", + "help" : "The frozen status of the \"creator\". This value is always initialized to 0." + } + }, + "local" : { + "balance" : { + "key" : "bl", + "kind" : "integer", + "help" : "The balance of the account." + }, + "frozen" : { + "key" : "fz", + "kind" : "integer", + "help" : "The frozen status of the account. This value is initialized to \"default-frozen\" when the account is first created. This value may be modified via the \"freeze\" procedure." + } + } + } +} diff --git a/test/scripts/e2e_subs/tealprogs/asa_approve.teal b/test/scripts/e2e_subs/tealprogs/asa_approve.teal new file mode 100644 index 0000000000..710405f797 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/asa_approve.teal @@ -0,0 +1,532 @@ +#pragma version 2 +txn NumAppArgs +int 6 +== +bnz if0 +txn ApplicationID +int 0 +== +! +bnz assert2 +err +assert2: +txn NumAccounts +int 0 +== +bnz cond4 +txn NumAccounts +int 1 +== +bnz cond5 +txn NumAppArgs +int 2 +== +bnz cond6 +// transfer asset +txna ApplicationArgs 0 +btoi +store 1 +load 1 +int 0 +== +bnz unless7 +// cannot modify frozen asset +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if9 +int 0 +byte base64 Zno= +app_local_get +int 1 +== +int 1 +bnz if_end10 +if9: +byte base64 Zno= +app_global_get +int 1 +== +if_end10: +! +bnz assert8 +err +assert8: +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if11 +int 0 +byte base64 Ymw= +int 0 +byte base64 Ymw= +app_local_get +load 1 +- +app_local_put +int 1 +bnz if_end12 +if11: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 1 +- +app_global_put +if_end12: +unless7: +load 1 +int 0 +== +bnz unless13 +// cannot modify frozen asset +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if15 +int 1 +byte base64 Zno= +app_local_get +int 1 +== +int 1 +bnz if_end16 +if15: +byte base64 Zno= +app_global_get +int 1 +== +if_end16: +! +bnz assert14 +err +assert14: +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if17 +int 1 +byte base64 Ymw= +int 1 +byte base64 Ymw= +app_local_get +load 1 ++ +app_local_put +int 1 +bnz if_end18 +if17: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 1 ++ +app_global_put +if_end18: +unless13: +txna Accounts 2 +global ZeroAddress +== +bnz unless19 +int 0 +byte base64 Ymw= +app_local_get +store 2 +load 2 +int 0 +== +bnz unless20 +// cannot modify frozen asset +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if22 +int 0 +byte base64 Zno= +app_local_get +int 1 +== +int 1 +bnz if_end23 +if22: +byte base64 Zno= +app_global_get +int 1 +== +if_end23: +! +bnz assert21 +err +assert21: +txn Sender +byte base64 Y3I= +app_global_get +== +bnz if24 +int 0 +byte base64 Ymw= +int 0 +byte base64 Ymw= +app_local_get +load 2 +- +app_local_put +int 1 +bnz if_end25 +if24: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 2 +- +app_global_put +if_end25: +unless20: +load 2 +int 0 +== +bnz unless26 +// cannot modify frozen asset +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if28 +int 2 +byte base64 Zno= +app_local_get +int 1 +== +int 1 +bnz if_end29 +if28: +byte base64 Zno= +app_global_get +int 1 +== +if_end29: +! +bnz assert27 +err +assert27: +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if30 +int 2 +byte base64 Ymw= +int 2 +byte base64 Ymw= +app_local_get +load 2 ++ +app_local_put +int 1 +bnz if_end31 +if30: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 2 ++ +app_global_put +if_end31: +unless26: +unless19: +txn NumAppArgs +int 1 +== +txn NumAccounts +int 2 +== +&& +txn OnCompletion +int 0 +== +bnz if32 +txn OnCompletion +int 2 +== +int 0 +byte base64 Ymw= +app_local_get +int 0 +== +&& +txna Accounts 2 +global ZeroAddress +== +! +&& +int 1 +bnz if_end33 +if32: +txna Accounts 2 +global ZeroAddress +== +if_end33: +&& +int 1 +bnz cond_end3 +cond6: +// clawback asset +txna ApplicationArgs 0 +btoi +store 0 +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if34 +int 1 +byte base64 Ymw= +int 1 +byte base64 Ymw= +app_local_get +load 0 +- +app_local_put +int 1 +bnz if_end35 +if34: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 0 +- +app_global_put +if_end35: +txna Accounts 2 +byte base64 Y3I= +app_global_get +== +bnz if36 +int 2 +byte base64 Ymw= +int 2 +byte base64 Ymw= +app_local_get +load 0 ++ +app_local_put +int 1 +bnz if_end37 +if36: +byte base64 Ymw= +byte base64 Ymw= +app_global_get +load 0 ++ +app_global_put +if_end37: +txn NumAccounts +int 2 +== +txn OnCompletion +int 0 +== +&& +txn Sender +byte base64 Y2w= +app_global_get +== +&& +int 1 +bnz cond_end3 +cond5: +// freeze asset holding +txna Accounts 1 +byte base64 Y3I= +app_global_get +== +bnz if38 +int 1 +byte base64 Zno= +txna ApplicationArgs 0 +btoi +app_local_put +int 1 +bnz if_end39 +if38: +byte base64 Zno= +txna ApplicationArgs 0 +btoi +app_global_put +if_end39: +txn NumAppArgs +int 1 +== +txn OnCompletion +int 0 +== +&& +txn Sender +byte base64 ZnI= +app_global_get +== +&& +int 1 +bnz cond_end3 +cond4: +// asset deletion or opt-in +txn OnCompletion +int 1 +== +! +bnz when40 +// opting in to implicit zero bl +int 0 +byte base64 Zno= +byte base64 ZGY= +app_global_get +app_local_put +when40: +txn NumAppArgs +int 0 +== +txn OnCompletion +int 5 +== +txn Sender +byte base64 bW4= +app_global_get +== +&& +byte base64 dHQ= +app_global_get +byte base64 Ymw= +app_global_get +== +&& +txn OnCompletion +int 1 +== +txn Sender +byte base64 Y3I= +app_global_get +== +! +&& +|| +&& +cond_end3: +int 1 +bnz if_end1 +if0: +// asset configuration +txn ApplicationID +int 0 +== +bnz if41 +txn Sender +byte base64 bW4= +app_global_get +== +txna ApplicationArgs 0 +global ZeroAddress +== +byte base64 bW4= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 1 +global ZeroAddress +== +byte base64 cnY= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 2 +global ZeroAddress +== +byte base64 ZnI= +app_global_get +global ZeroAddress +== +! +|| +&& +txna ApplicationArgs 3 +global ZeroAddress +== +byte base64 Y2w= +app_global_get +global ZeroAddress +== +! +|| +&& +bnz assert43 +err +assert43: +int 1 +bnz if_end42 +if41: +byte base64 Y3I= +txn Sender +app_global_put +byte base64 dHQ= +txna ApplicationArgs 4 +btoi +app_global_put +byte base64 Ymw= +txna ApplicationArgs 4 +btoi +app_global_put +byte base64 ZGY= +txna ApplicationArgs 5 +btoi +app_global_put +if_end42: +byte base64 bW4= +txna ApplicationArgs 0 +app_global_put +byte base64 cnY= +txna ApplicationArgs 1 +app_global_put +byte base64 ZnI= +txna ApplicationArgs 2 +app_global_put +byte base64 Y2w= +txna ApplicationArgs 3 +app_global_put +txn NumAccounts +int 0 +== +txn OnCompletion +int 0 +== +&& +txna ApplicationArgs 0 +len +int 32 +== +&& +txna ApplicationArgs 1 +len +int 32 +== +&& +txna ApplicationArgs 2 +len +int 32 +== +&& +txna ApplicationArgs 3 +len +int 32 +== +&& +if_end1: diff --git a/test/scripts/e2e_subs/tealprogs/asa_clear.teal b/test/scripts/e2e_subs/tealprogs/asa_clear.teal new file mode 100644 index 0000000000..999b8312cd --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/asa_clear.teal @@ -0,0 +1,10 @@ +#pragma version 2 +byte base64 Ymw= +byte base64 Ymw= +app_global_get +int 0 +byte base64 Ymw= +app_local_get ++ +app_global_put +int 1 diff --git a/test/scripts/e2e_subs/tealprogs/assetround.teal b/test/scripts/e2e_subs/tealprogs/assetround.teal new file mode 100644 index 0000000000..0b1779d33b --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/assetround.teal @@ -0,0 +1,121 @@ +#pragma version 2 +// +// Balance +// + +// Check sender's account +int 0 + +// Check app ID as passed in +txna ApplicationArgs 0 +btoi + +// Check balance +asset_holding_get AssetBalance +pop + +// Sender should have correct balance (arg 1) +txna ApplicationArgs 1 +btoi +== +bz fail + +// Check sender's account +int 0 + +// Check app ID as passed in +txna ApplicationArgs 0 +btoi + +// Check frozen status +asset_holding_get AssetFrozen +pop + +// Sender should have correct frozen status (arg 2) +txna ApplicationArgs 2 +btoi +== +bz fail + +// If we shouldn't check params (arg 3), then skip to check round +txna ApplicationArgs 3 +btoi +bnz round + +// +// Params +// + +// Check sender's account (creator) +int 0 + +// Check app ID as passed in +txna ApplicationArgs 0 +btoi + +// Check total against arg 4 +asset_params_get AssetTotal +pop +txna ApplicationArgs 4 +btoi +== +bz fail + +// Check sender's account +int 0 + +// Check app ID as passed in +txna ApplicationArgs 0 +btoi + +// Check name against arg 5 +asset_params_get AssetUnitName +pop +txna ApplicationArgs 5 +== +bz fail + +round: + +// Check round against arg 6 (arg < global Round, arg + 4 > global Round) +txna ApplicationArgs 6 +btoi + +global Round +< +bz fail + +txna ApplicationArgs 6 +btoi + +int 4 ++ + +// Check timestamp against arg 7 (arg < global LatestTimestamp + 60, arg + 60 > global LatestTimestamp) +txna ApplicationArgs 7 +btoi + +global LatestTimestamp +int 60 ++ + +< +bz fail + +txna ApplicationArgs 7 +btoi + +int 60 ++ + +global LatestTimestamp +> +bz fail + +success: +int 1 +return + +fail: +int 0 +return diff --git a/test/scripts/e2e_subs/tealprogs/bootloader.teal.tmpl b/test/scripts/e2e_subs/tealprogs/bootloader.teal.tmpl new file mode 100644 index 0000000000..4d65c6ad4d --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/bootloader.teal.tmpl @@ -0,0 +1,42 @@ +#pragma version 2 +// Creation: always succeed, since this can only happen once +txn ApplicationID +int 0 +== + +// TODO: allow initializing GlobalState? +// TODO: care about sender? +bnz success + +// Not creation: update programs + +// Ensure OnCompletion is UpdateApplication +txn OnCompletion +int 4 +== +bz fail + +// Ensure txn's ApprovalProgram is correct +txn ApprovalProgram +sha256 +byte 0xTMPL_APPROV_HASH +== +bz fail + +// Ensure txn's ClearStateProgram is correct +txn ClearStateProgram +sha256 +byte 0xTMPL_CLEARSTATE_HASH +== +bz fail + +// Approved +b success + +success: +int 1 +return + +fail: +int 0 +return diff --git a/test/scripts/e2e_subs/tealprogs/globcheck.teal b/test/scripts/e2e_subs/tealprogs/globcheck.teal new file mode 100644 index 0000000000..94ef4a4eb9 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/globcheck.teal @@ -0,0 +1,69 @@ +#pragma version 2 +// This program approves all transactions whose first arg is "hello" +// Then, accounts can write "foo": "bar" to the GlobalState by +// sending a transaction whose first argument is "write". Finally, +// accounts can send the args ["check", xyz] to confirm that the +// key at "foo" is equal to the second argument, xyz + +// If arg 0 is "hello" +txna ApplicationArgs 0 +byte base64 aGVsbG8= +== +bnz succeed + +// else + +// If arg 0 is "write" +txna ApplicationArgs 0 +byte base64 d3JpdGU= +== +bnz write + +// else + +// arg 0 must be "check" +txna ApplicationArgs 0 +byte base64 Y2hlY2s= +== + +// and arg 1 must be the value at "foo" +// Key "foo" +int 0 +byte base64 Zm9v +app_global_get_ex + +// Value must exist +int 0 +== +bnz fail + +// Value must equal arg +txna ApplicationArgs 1 +== +&& + +int 1 +bnz done + +write: +// Write to GlobalState + +// Key "foo" +byte base64 Zm9v + +// Value "bar" +byte base64 YmFy +app_global_put + +int 1 +bnz succeed + +succeed: +int 1 +int 1 +bnz done + +fail: +int 0 + +done: diff --git a/test/scripts/e2e_subs/tealprogs/globwrite.teal b/test/scripts/e2e_subs/tealprogs/globwrite.teal new file mode 100644 index 0000000000..443b3e7e74 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/globwrite.teal @@ -0,0 +1,12 @@ +#pragma version 2 +// Key is "foo" +byte base64 Zm9v + +// Value is ApplicationArgs 0 +txna ApplicationArgs 0 + +// Write it +app_global_put + +int 1 +return diff --git a/test/scripts/e2e_subs/tealprogs/loccheck.teal b/test/scripts/e2e_subs/tealprogs/loccheck.teal new file mode 100644 index 0000000000..a5aa2e3e7e --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/loccheck.teal @@ -0,0 +1,79 @@ +#pragma version 2 +// This program approves all transactions whose first arg is "hello" +// Then, accounts can write "foo": "bar" to their LocalState by +// sending a transaction whose first argument is "write". Finally, +// accounts can send the args ["check", xyz] to confirm that the +// key at "foo" is equal to the second argument, xyz + +// If arg 0 is "hello" +txna ApplicationArgs 0 +byte base64 aGVsbG8= +== +bnz succeed + +// else + +// If arg 0 is "write" +txna ApplicationArgs 0 +byte base64 d3JpdGU= +== +bnz write + +// else + +// arg 0 must be "check" +txna ApplicationArgs 0 +byte base64 Y2hlY2s= +== + +// and arg 1 must be the value at "foo" +// txn.Sender +int 0 + +// App ID (this app) +int 0 + +// Key "foo" +byte base64 Zm9v +app_local_get_ex + +// Value must exist +int 0 +== +bnz fail + +// Value must equal arg +txna ApplicationArgs 1 +== +&& + +int 1 +bnz done + +write: +// Write to our LocalState + +// txn.Sender +int 0 + +// Key "foo" +byte base64 Zm9v + +// Value "bar" +byte base64 YmFy +app_local_put + +int 1 +bnz succeed + +succeed: +int 1 +int 1 +bnz done + +fail: +int 0 +int 1 +bnz done + +done: diff --git a/test/scripts/e2e_subs/tealprogs/sectok.json b/test/scripts/e2e_subs/tealprogs/sectok.json new file mode 100644 index 0000000000..ef0d74c293 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/sectok.json @@ -0,0 +1,414 @@ +{ + "query": { + "global": { + "lock-until-group": { + "map": { + "kind": "byte", + "prefix": "0x0001" + }, + "size": 40, + "kind": "int", + "help": "The first timestamp at which accounts in one group can issue transfers to accounts in another group." + }, + "token-params": { + "kind": "string", + "key": "px", + "help": "The parameters of the security token. This is a JSON object with the string \"symbol\" attribute, the string \"name\" attribute, and the integer \"decimals\" attribute." + }, + "reserve-supply": { + "kind": "int", + "key": "rv", + "help": "The supply of this security token currently sitting in reserve." + }, + "total-supply": { + "kind": "int", + "key": "tt", + "help": "The total supply of this security token when first created. This value is constant." + }, + "paused": { + "kind": "int", + "key": "ps", + "help": "Whether all trading is paused. No trading occurs unless this is zero." + } + }, + "local": { + "lock-until": { + "kind": "int", + "key": "tl", + "help": "The first timestamp at which this account can issue and accept transfers." + }, + "contract-admin": { + "kind": "int", + "key": "cX", + "help": "Whether this account is a contract admin. If this is one, this account may upgrade the application code, set the contract and transfer admin status of accounts, destroy the asset if the reserve holds the supply, mint and burn from the supply, pause and unpause all transfers, and freeze and unfreeze accounts. This is automatically set to one for the token creator." + }, + "frozen": { + "kind": "int", + "key": "fz", + "help": "Whether this account is frozen. This account issue and accept transfers unless this is zero." + }, + "balance": { + "kind": "int", + "key": "bl", + "help": "The number of tokens held by this account." + }, + "transfer-admin": { + "kind": "int", + "key": "tX", + "help": "Whether this account is a transfer admin. A transfer admin can set transfer rules between groups, freeze and unfreeze accounts, and set the max balance, transfer group, and timed locks for accounts." + }, + "transfer-group": { + "kind": "int", + "key": "tg", + "help": "The transfer group of this account, which affects the set of allowed transfers to and from other groups." + }, + "max-balance": { + "kind": "int", + "key": "mb", + "help": "The maximum token balance allowed for this account." + } + } + }, + "execute": { + "clear": { + "on-completion": "ClearState", + "accounts": [], + "foreign": [], + "create": false, + "help": "Clear all tokens out of the account.", + "args": [] + }, + "destroy": { + "on-completion": "DeleteApplication", + "accounts": [], + "foreign": [], + "create": false, + "help": "Destroy a security token. The entire token supply must be in the reserve. This transaction must be sent by a contract admin.", + "args": [] + }, + "set-transfer-rule": { + "on-completion": "NoOp", + "accounts": [], + "foreign": [], + "create": false, + "help": "Create, update, or delete a transfer rule. This transaction must be sent by a transfer admin.", + "args": [ + { + "name": "send-group", + "pseudo": false, + "kind": "int", + "help": "The sending group the rule applies to." + }, + { + "name": "receive-group", + "pseudo": false, + "kind": "int", + "help": "The receiving group the rule applies to." + }, + { + "name": "lock-until", + "pseudo": false, + "kind": "int", + "help": "The first timestamp at which accounts in the sending group may make transfers to accounts in the receiving group." + } + ] + }, + "pause": { + "on-completion": "NoOp", + "accounts": [], + "foreign": [], + "create": false, + "help": "Control whether all transfers are disallowed.", + "args": [ + { + "name": "paused", + "pseudo": false, + "kind": "int", + "help": "No transfers will be allowed unless this is zero." + } + ] + }, + "opt-in": { + "on-completion": "OptIn", + "accounts": [], + "foreign": [], + "create": false, + "help": "Opt into a token. The initial transfer group of all accounts is zero.", + "args": [] + }, + "mint": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to which to mint to." + } + ], + "foreign": [], + "create": false, + "help": "Mint funds into an account. This circumvents other restrictions on that account. This transaction must be sent by a contract admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "mn" + }, + { + "name": "amount", + "pseudo": false, + "kind": "int", + "help": "The number of tokens to mint into the account." + } + ] + }, + "set-transfer-admin": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to configure." + } + ], + "foreign": [], + "create": false, + "help": "Configure the transfer admin status of an account. This transaction must be sent by a contract admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "tX" + }, + { + "name": "status", + "pseudo": false, + "kind": "int", + "help": "If this is 1, confer transfer admin status on the account; otherwise, revoke transfer admin status on the account." + } + ] + }, + "set-contract-admin": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to configure." + } + ], + "foreign": [], + "create": false, + "help": "Configure the contract admin status of an account. This transaction must be sent by a contract admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "cX" + }, + { + "name": "status", + "pseudo": false, + "kind": "int", + "help": "If this is 1, confer contract admin status on the account; otherwise, revoke contract admin status on the account." + } + ] + }, + "transfer": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "receiver", + "pseudo": false, + "help": "The account receiving the tokens." + } + ], + "foreign": [], + "create": false, + "help": "Transfer tokens to a receiver.", + "args": [ + { + "name": "amount", + "pseudo": false, + "kind": "int", + "help": "The number of tokens to transfer." + } + ] + }, + "upgrade": { + "on-completion": "UpdateApplication", + "accounts": [], + "foreign": [], + "create": false, + "help": "Update the programs implementing the token. This transaction must be sent by a contract admin.", + "args": [] + }, + "set-lock-until": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to configure." + } + ], + "foreign": [], + "create": false, + "help": "Set the first timestamp at which an account can issue transactions. This transaction must be sent by a transfer admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "tl" + }, + { + "name": "lock-until", + "pseudo": false, + "kind": "int", + "help": "Transfers from the account will be restricted until this timestamp passes." + } + ] + }, + "set-max-balance": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to configure." + } + ], + "foreign": [], + "create": false, + "help": "Set the maximum balance of an account. This transaction must be sent by a transfer admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "mb" + }, + { + "name": "max-balance", + "pseudo": false, + "kind": "int", + "help": "The new maximum balance of the account." + } + ] + }, + "create": { + "on-completion": "OptIn", + "accounts": [], + "foreign": [], + "create": true, + "help": "Create a new security token.", + "args": [ + { + "name": "token-params", + "pseudo": false, + "kind": "string", + "help": "The parameters for the token." + }, + { + "name": "total-supply", + "pseudo": false, + "kind": "int", + "help": "The total supply for the token." + } + ] + }, + "burn": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account from which to mint from." + } + ], + "foreign": [], + "create": false, + "help": "Burn funds from an account. This circumvents other restrictions on that account. This transaction must be sent by a contract admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "br" + }, + { + "name": "amount", + "pseudo": false, + "kind": "int", + "help": "The number of tokens to burn from the account." + } + ] + }, + "freeze": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account which to freeze/unfreeze." + } + ], + "foreign": [], + "create": false, + "help": "Freeze or unfreeze an account. This transaction must be sent by either a contract or a transfer admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "fz" + }, + { + "name": "frozen", + "pseudo": false, + "kind": "int", + "help": "No transfers from this account will be allowed unless this is zero." + } + ] + }, + "set-transfer-group": { + "on-completion": "NoOp", + "accounts": [ + { + "name": "target", + "pseudo": false, + "help": "The account to configure." + } + ], + "foreign": [], + "create": false, + "help": "Set the transfer group an account belongs to. This transaction must be sent by a transfer admin.", + "args": [ + { + "name": "ignored", + "pseudo": true, + "kind": "string", + "help": "", + "default": "tg" + }, + { + "name": "transfer-group", + "pseudo": false, + "kind": "int", + "help": "The new transfer group of the account." + } + ] + } + } +} diff --git a/test/scripts/e2e_subs/tealprogs/sectok_approve.teal b/test/scripts/e2e_subs/tealprogs/sectok_approve.teal new file mode 100644 index 0000000000..baf27cb220 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/sectok_approve.teal @@ -0,0 +1,386 @@ +#pragma version 2 + +txn ApplicationID +int 0 +== +bnz cond1 +txn OnCompletion +int 5 +== +bnz cond2 +txn OnCompletion +int 4 +== +bnz cond3 +txn OnCompletion +int 1 +== +bnz cond4 +txn OnCompletion +int 0 +== +txn NumAccounts +int 2 +< +&& +bnz assert5 +err +assert5: +txn NumAccounts +int 0 +== +bnz if6 +txn NumAppArgs +int 1 +== +bnz if8 +// config +txn NumAppArgs +int 2 +== +bnz assert10 +err +assert10: +txna ApplicationArgs 0 +byte "cX" +== +txna ApplicationArgs 0 +byte "tX" +== +|| +bnz cond12 +txna ApplicationArgs 0 +byte "mn" +== +bnz cond13 +txna ApplicationArgs 0 +byte "br" +== +bnz cond14 +// set max balance, lock until, transfer group, freeze +int 1 +txna ApplicationArgs 0 +txna ApplicationArgs 1 +btoi +app_local_put +txna ApplicationArgs 0 +byte "fz" +== +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +int 0 +byte base64 dFg= +app_local_get +int 1 +== +|| +&& +int 0 +byte base64 dFg= +app_local_get +int 1 +== +txna ApplicationArgs 0 +byte "mb" +== +txna ApplicationArgs 0 +byte "tl" +== +|| +txna ApplicationArgs 0 +byte "tg" +== +|| +&& +|| +int 1 +bnz cond_end11 +cond14: +// burn +int 1 +byte base64 Ymw= +int 1 +byte base64 Ymw= +app_local_get +txna ApplicationArgs 1 +btoi +- +app_local_put +byte base64 cnY= +byte base64 cnY= +app_global_get +txna ApplicationArgs 1 +btoi ++ +app_global_put +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +int 1 +bnz cond_end11 +cond13: +// mint +int 1 +byte base64 Ymw= +int 1 +byte base64 Ymw= +app_local_get +txna ApplicationArgs 1 +btoi ++ +app_local_put +byte base64 cnY= +byte base64 cnY= +app_global_get +txna ApplicationArgs 1 +btoi +- +app_global_put +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +int 1 +bnz cond_end11 +cond12: +// contract/transfer admin +int 1 +txna ApplicationArgs 0 +txna ApplicationArgs 1 +btoi +app_local_put +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +cond_end11: +int 1 +bnz if_end9 +if8: +// transfer +int 0 +byte base64 Ymw= +int 0 +byte base64 Ymw= +app_local_get +txna ApplicationArgs 0 +btoi +- +app_local_put +int 1 +byte base64 Ymw= +int 1 +byte base64 Ymw= +app_local_get +txna ApplicationArgs 0 +btoi ++ +app_local_put +byte base64 cHM= +app_global_get +int 0 +== +int 1 +byte base64 Ymw= +app_local_get +int 1 +byte base64 bWI= +app_local_get +<= +&& +int 0 +byte base64 Zno= +app_local_get +int 0 +== +&& +int 1 +byte base64 Zno= +app_local_get +int 0 +== +&& +int 0 +byte base64 dGw= +app_local_get +global LatestTimestamp +< +&& +int 1 +byte base64 dGw= +app_local_get +global LatestTimestamp +< +&& +byte 0x0001 +int 0 +byte base64 dGc= +app_local_get +itob +concat +int 1 +byte base64 dGc= +app_local_get +itob +concat +app_global_get +int 1 +- +global LatestTimestamp +< +&& +if_end9: +int 1 +bnz if_end7 +if6: +txn NumAppArgs +int 1 +== +bnz if15 +// transfer-rule +txna ApplicationArgs 2 +btoi +int 0 +== +bnz if17 +byte 0x0001 +txna ApplicationArgs 0 +concat +txna ApplicationArgs 1 +concat +txna ApplicationArgs 2 +btoi +app_global_put +int 1 +bnz if_end18 +if17: +byte 0x0001 +txna ApplicationArgs 0 +concat +txna ApplicationArgs 1 +concat +app_global_del +if_end18: +txna ApplicationArgs 0 +len +int 8 +== +txna ApplicationArgs 1 +len +int 8 +== +&& +txn NumAppArgs +int 3 +== +&& +int 0 +byte base64 dFg= +app_local_get +int 1 +== +&& +int 1 +bnz if_end16 +if15: +// pause +byte base64 cHM= +txna ApplicationArgs 0 +btoi +app_global_put +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +if_end16: +if_end7: +int 1 +bnz cond_end0 +cond4: +// opt-in +txn NumAppArgs +int 0 +== +txn NumAccounts +int 0 +== +&& +int 1 +bnz cond_end0 +cond3: +// upgrade +txn NumAppArgs +int 0 +== +txn NumAccounts +int 0 +== +&& +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +&& +int 1 +bnz cond_end0 +cond2: +// destroy +txn NumAppArgs +int 0 +== +txn NumAccounts +int 0 +== +&& +int 0 +byte base64 Y1g= +app_local_get +int 1 +== +&& +byte base64 cnY= +app_global_get +byte base64 dHQ= +app_global_get +== +&& +int 1 +bnz cond_end0 +cond1: +// create +int 0 +byte base64 Y1g= +int 1 +app_local_put +byte base64 cHg= +txna ApplicationArgs 0 +app_global_put +byte base64 cnY= +txna ApplicationArgs 1 +btoi +app_global_put +byte base64 dHQ= +txna ApplicationArgs 1 +btoi +app_global_put +txn NumAppArgs +int 2 +== +txn NumAccounts +int 0 +== +&& +txn OnCompletion +int 1 +== +&& +cond_end0: diff --git a/test/scripts/e2e_subs/tealprogs/sectok_clear.teal b/test/scripts/e2e_subs/tealprogs/sectok_clear.teal new file mode 100644 index 0000000000..79d6193fa9 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/sectok_clear.teal @@ -0,0 +1,10 @@ +#pragma version 2 + +byte base64 cnY= +byte base64 cnY= +app_global_get +int 0 +byte base64 Ymw= +app_local_get ++ +app_global_put diff --git a/test/scripts/e2e_subs/tealprogs/upgraded.teal b/test/scripts/e2e_subs/tealprogs/upgraded.teal new file mode 100644 index 0000000000..8ee463eca7 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/upgraded.teal @@ -0,0 +1,12 @@ +#pragma version 2 +// Key is "foo" +byte base64 Zm9v + +// Value is "foo" +byte base64 Zm9v + +// Write it +app_global_put + +int 1 +return diff --git a/test/scripts/e2e_subs/tealprogs/wrongupgrade.teal b/test/scripts/e2e_subs/tealprogs/wrongupgrade.teal new file mode 100644 index 0000000000..b17d4a8988 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/wrongupgrade.teal @@ -0,0 +1,12 @@ +#pragma version 2 +// Key is "foo" +byte base64 Zm9v + +// Value is "xxx" +byte base64 eHh4 + +// Write it +app_global_put + +int 1 +return diff --git a/test/scripts/e2e_subs/tealprogs/xappreads.teal b/test/scripts/e2e_subs/tealprogs/xappreads.teal new file mode 100644 index 0000000000..e8a1fe3e40 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/xappreads.teal @@ -0,0 +1,27 @@ +#pragma version 2 +// Fetch app idx we want to read global state from +int 1 // ForeignApps index +// Fetch key "foo" +byte base64 Zm9v + +// Get value +app_global_get_ex + +// Should exist +bz fail + +// Value should be "bar" +byte base64 YmFy +== +bz fail + +// Test passed +b succeed + +fail: +int 0 +return + +succeed: +int 1 +return From 97712d591c16e5c29ba6baa32d6c3d9be750d516 Mon Sep 17 00:00:00 2001 From: Derek Leung Date: Fri, 10 Jul 2020 11:12:00 -0400 Subject: [PATCH 094/267] Rename AppStateChage to AppStateChange. (#1235) This change fixes a presumed typo. --- cmd/tealdbg/cdtSession.go | 2 +- cmd/tealdbg/debugger.go | 4 ++-- data/transactions/logic/debugger.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/tealdbg/cdtSession.go b/cmd/tealdbg/cdtSession.go index 0a1d84fe3b..985e4275fa 100644 --- a/cmd/tealdbg/cdtSession.go +++ b/cmd/tealdbg/cdtSession.go @@ -242,7 +242,7 @@ func (s *cdtSession) websocketHandler(w http.ResponseWriter, r *http.Request) { case <-cdtUpdatedCh: dbgStateMu.Lock() - appState := s.debugger.GetStates(&dbgState.AppStateChage) + appState := s.debugger.GetStates(&dbgState.AppStateChange) state.Update(cdtStateUpdate{ dbgState.Stack, dbgState.Scratch, dbgState.PC, dbgState.Line, dbgState.Error, diff --git a/cmd/tealdbg/debugger.go b/cmd/tealdbg/debugger.go index 3b5d720697..f5c6876d44 100644 --- a/cmd/tealdbg/debugger.go +++ b/cmd/tealdbg/debugger.go @@ -53,7 +53,7 @@ type Control interface { GetSourceMap() ([]byte, error) GetSource() (string, []byte) - GetStates(changes *logic.AppStateChage) appState + GetStates(changes *logic.AppStateChange) appState } // Debugger is TEAL event-driven debugger @@ -285,7 +285,7 @@ func (s *session) GetSource() (string, []byte) { return s.programName, []byte(s.source) } -func (s *session) GetStates(changes *logic.AppStateChage) appState { +func (s *session) GetStates(changes *logic.AppStateChange) appState { if changes == nil { return s.states } diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go index 98b4a54469..5e72754542 100644 --- a/data/transactions/logic/debugger.go +++ b/data/transactions/logic/debugger.go @@ -76,11 +76,11 @@ type DebugState struct { Error string `codec:"error"` // global/local state changes are updated every step. Stateful TEAL only. - AppStateChage + AppStateChange } -// AppStateChage encapsulates global and local app state changes -type AppStateChage struct { +// AppStateChange encapsulates global and local app state changes +type AppStateChange struct { GlobalStateChanges basics.StateDelta `codec:"gsch"` LocalStateChanges map[basics.Address]basics.StateDelta `codec:"lsch"` } From 171c4afd8b64f6fa24528bd63f6b7407e162eba7 Mon Sep 17 00:00:00 2001 From: algomaxj <65551122+algomaxj@users.noreply.github.com> Date: Fri, 10 Jul 2020 13:06:31 -0400 Subject: [PATCH 095/267] fix TestTealCompile (#1236) A program with no #pragma version 2 should result in compiling a version 1 program. Co-authored-by: Max Justicz --- test/e2e-go/features/teal/compile_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/e2e-go/features/teal/compile_test.go b/test/e2e-go/features/teal/compile_test.go index 5b1f079bd2..e044f4c0d9 100644 --- a/test/e2e-go/features/teal/compile_test.go +++ b/test/e2e-go/features/teal/compile_test.go @@ -63,7 +63,13 @@ func TestTealCompile(t *testing.T) { var hash crypto.Digest compiledProgram, hash, err = libGoalClient.Compile([]byte("int 1")) a.NotNil(compiledProgram) - a.NoError(err, "A valid program should result in a compilation success") + a.NoError(err, "A valid v1 program should result in a compilation success") + a.Equal([]byte{0x1, 0x20, 0x1, 0x1, 0x22}, compiledProgram) + a.Equal("6Z3C3LDVWGMX23BMSYMANACQOSINPFIRF77H7N3AWJZYV6OH6GWQ", hash.String()) + + compiledProgram, hash, err = libGoalClient.Compile([]byte("#pragma version 2\nint 1")) + a.NotNil(compiledProgram) + a.NoError(err, "A valid v2 program should result in a compilation success") a.Equal([]byte{0x2, 0x20, 0x1, 0x1, 0x22}, compiledProgram) a.Equal("YOE6C22GHCTKAN3HU4SE5PGIPN5UKXAJTXCQUPJ3KKF5HOAH646A", hash.String()) From 12e6b969295e770d5841d5b68e04f93d97d7f4b5 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 11 Jul 2020 12:18:23 -0400 Subject: [PATCH 096/267] Add CatchpointDump utility (#1231) The catchpointdump utility allows to perform the following: Given a catchpoint file, it knows how to read it's content and dump the output in a human readable format into a separate file. Given a network name and a round number, it would fetch the catchpoint for that round from all the relays of that network, save the catchpoint file, and dump it's content. Given an ledger tracker database, it can dump the database content into a file --- cmd/catchpointdump/commands.go | 144 +++++ cmd/catchpointdump/database.go | 57 ++ cmd/catchpointdump/file.go | 225 ++++++++ cmd/catchpointdump/net.go | 160 ++++++ ledger/catchpointwriter.go | 8 +- ledger/catchpointwriter_test.go | 2 +- ledger/catchupaccessor.go | 2 +- ledger/msgp_gen.go | 906 ++++++++++++++++---------------- ledger/msgp_gen_test.go | 40 +- 9 files changed, 1066 insertions(+), 478 deletions(-) create mode 100644 cmd/catchpointdump/commands.go create mode 100644 cmd/catchpointdump/database.go create mode 100644 cmd/catchpointdump/file.go create mode 100644 cmd/catchpointdump/net.go diff --git a/cmd/catchpointdump/commands.go b/cmd/catchpointdump/commands.go new file mode 100644 index 0000000000..2122a780ae --- /dev/null +++ b/cmd/catchpointdump/commands.go @@ -0,0 +1,144 @@ +// 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 . + +package main + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/logging" +) + +var log = logging.Base() + +var dataDirs []string + +var defaultCacheDir = "goal.cache" + +var verboseVersionPrint bool + +var kmdDataDirFlag string + +var versionCheck bool + +func init() { + // file.go + rootCmd.AddCommand(fileCmd) + rootCmd.AddCommand(netCmd) + rootCmd.AddCommand(databaseCmd) + +} + +var rootCmd = &cobra.Command{ + Use: "catchpointdump", + Short: "Catchpoint dump utility", + Long: "Catchpoint dump utility", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + if versionCheck { + fmt.Println(config.FormatVersionAndLicense()) + return + } + //If no arguments passed, we should fallback to help + cmd.HelpFunc()(cmd, args) + }, +} + +// Write commands to exercise all subcommands with `-h` +// Can be used to check that there are no conflicts in arguments between inner and outer commands. +func runAllHelps(c *cobra.Command, out io.Writer) (err error) { + if c.Runnable() { + cmd := c.CommandPath() + " -h\n" + _, err = out.Write([]byte(cmd)) + if err != nil { + return + } + } + for _, sub := range c.Commands() { + err = runAllHelps(sub, out) + if err != nil { + return + } + } + return +} + +func main() { + // Hidden command to generate docs in a given directory + // goal generate-docs [path] + if len(os.Args) == 3 && os.Args[1] == "generate-docs" { + err := doc.GenMarkdownTree(rootCmd, os.Args[2]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) + } else if len(os.Args) == 2 && os.Args[1] == "helptest" { + // test that subcommands don't have arg conflicts: + // goal helptest | bash -x -e + runAllHelps(rootCmd, os.Stdout) + os.Exit(0) + } + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func reportInfoln(args ...interface{}) { + fmt.Println(args...) + // log.Infoln(args...) +} + +func reportInfof(format string, args ...interface{}) { + fmt.Printf(format+"\n", args...) + // log.Infof(format, args...) +} + +func reportWarnln(args ...interface{}) { + fmt.Print("Warning: ") + fmt.Println(args...) + // log.Warnln(args...) +} + +func reportWarnf(format string, args ...interface{}) { + fmt.Printf("Warning: "+format+"\n", args...) + // log.Warnf(format, args...) +} + +func reportErrorln(args ...interface{}) { + fmt.Fprintln(os.Stderr, args...) + // log.Warnln(args...) + os.Exit(1) +} + +func reportErrorf(format string, args ...interface{}) { + fmt.Fprintf(os.Stderr, format+"\n", args...) + // log.Warnf(format, args...) + os.Exit(1) +} + +// validateNoPosArgsFn is a reusable cobra positional argument validation function +// for generating proper error messages when commands see unexpected arguments when they expect no args. +// We don't use cobra.NoArgs directly, in case we want to customize behavior later. +var validateNoPosArgsFn = cobra.NoArgs diff --git a/cmd/catchpointdump/database.go b/cmd/catchpointdump/database.go new file mode 100644 index 0000000000..401cee25a6 --- /dev/null +++ b/cmd/catchpointdump/database.go @@ -0,0 +1,57 @@ +// 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 . + +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/ledger" +) + +var ledgerTrackerFilename string + +func init() { + databaseCmd.Flags().StringVarP(&ledgerTrackerFilename, "tracker", "t", "", "Specify the ledger tracker file name ( i.e. ./ledger.tracker.sqlite )") + databaseCmd.Flags().StringVarP(&outFileName, "output", "o", "", "Specify an outfile for the dump ( i.e. ledger.dump.txt )") +} + +var databaseCmd = &cobra.Command{ + Use: "database", + Short: "Dump the given ledger tracker database", + Long: "Dump the given ledger tracker database", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + if ledgerTrackerFilename == "" { + cmd.HelpFunc()(cmd, args) + return + } + outFile := os.Stdout + var err error + if outFileName != "" { + outFile, err = os.OpenFile(outFileName, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + reportErrorf("Unable to create file '%s' : %v", outFileName, err) + } + } + err = printAccountsDatabase(ledgerTrackerFilename, ledger.CatchpointFileHeader{}, outFile) + if err != nil { + reportErrorf("Unable to print account database : %v", err) + } + }, +} diff --git a/cmd/catchpointdump/file.go b/cmd/catchpointdump/file.go new file mode 100644 index 0000000000..bba3a444fc --- /dev/null +++ b/cmd/catchpointdump/file.go @@ -0,0 +1,225 @@ +// 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 . + +package main + +import ( + "archive/tar" + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" +) + +var tarFile string +var outFileName string + +func init() { + fileCmd.Flags().StringVarP(&tarFile, "tar", "t", "", "Specify the tar file to process") + fileCmd.Flags().StringVarP(&outFileName, "output", "o", "", "Specify an outfile for the dump ( i.e. tracker.dump.txt )") +} + +var fileCmd = &cobra.Command{ + Use: "file", + Short: "Specify a file to dump", + Long: "Specify a file to dump", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + if tarFile == "" { + cmd.HelpFunc()(cmd, args) + return + } + tarFileBytes, err := ioutil.ReadFile(tarFile) + if err != nil || len(tarFileBytes) == 0 { + reportErrorf("Unable to read '%s' : %v", tarFile, err) + } + genesisInitState := ledger.InitState{} + cfg := config.GetDefaultLocal() + l, err := ledger.OpenLedger(logging.Base(), "./ledger", false, genesisInitState, cfg) + if err != nil { + reportErrorf("Unable to open ledger : %v", err) + } + + defer os.Remove("./ledger.block.sqlite") + defer os.Remove("./ledger.block.sqlite-shm") + defer os.Remove("./ledger.block.sqlite-wal") + defer os.Remove("./ledger.tracker.sqlite") + defer os.Remove("./ledger.tracker.sqlite-shm") + defer os.Remove("./ledger.tracker.sqlite-wal") + defer l.Close() + + catchupAccessor := ledger.MakeCatchpointCatchupAccessor(l, logging.Base()) + err = catchupAccessor.ResetStagingBalances(context.Background(), true) + if err != nil { + reportErrorf("Unable to initialize catchup database : %v", err) + } + var fileHeader ledger.CatchpointFileHeader + fileHeader, err = loadCatchpointIntoDatabase(context.Background(), catchupAccessor, tarFileBytes) + if err != nil { + reportErrorf("Unable to load catchpoint file into in-memory database : %v", err) + } + + outFile := os.Stdout + if outFileName != "" { + outFile, err = os.OpenFile(outFileName, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + reportErrorf("Unable to create file '%s' : %v", outFileName, err) + } + } + + err = printAccountsDatabase("./ledger.tracker.sqlite", fileHeader, outFile) + if err != nil { + reportErrorf("Unable to print account database : %v", err) + } + + }, +} + +func loadCatchpointIntoDatabase(ctx context.Context, catchupAccessor ledger.CatchpointCatchupAccessor, fileBytes []byte) (fileHeader ledger.CatchpointFileHeader, err error) { + reader := bytes.NewReader(fileBytes) + tarReader := tar.NewReader(reader) + var downloadProgress ledger.CatchpointCatchupAccessorProgress + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + return fileHeader, nil + } + return fileHeader, err + } + balancesBlockBytes := make([]byte, header.Size) + readComplete := int64(0) + + for readComplete < header.Size { + bytesRead, err := tarReader.Read(balancesBlockBytes[readComplete:]) + readComplete += int64(bytesRead) + if err != nil { + if err == io.EOF { + if readComplete == header.Size { + break + } + err = fmt.Errorf("getPeerLedger received io.EOF while reading from tar file stream prior of reaching chunk size %d / %d", readComplete, header.Size) + } + return fileHeader, err + } + } + err = catchupAccessor.ProgressStagingBalances(ctx, header.Name, balancesBlockBytes, &downloadProgress) + if err != nil { + return fileHeader, err + } + if header.Name == "content.msgpack" { + // we already know it's valid, since we validated that above. + protocol.Decode(balancesBlockBytes, &fileHeader) + } + } +} + +func printAccountsDatabase(databaseName string, fileHeader ledger.CatchpointFileHeader, outFile *os.File) error { + dbAccessor, err := db.MakeAccessor(databaseName, true, false) + if err != nil || dbAccessor.Handle == nil { + return err + } + if fileHeader.Version != 0 { + fmt.Fprintf(outFile, "Version: %d\nBalances Round: %d\nBlock Round: %d\nBlock Header Digest: %s\nCatchpoint: %s\nTotal Accounts: %d\nTotal Chunks: %d\n", + fileHeader.Version, + fileHeader.BalancesRound, + fileHeader.BlocksRound, + fileHeader.BlockHeaderDigest.String(), + fileHeader.Catchpoint, + fileHeader.TotalAccounts, + fileHeader.TotalChunks) + + totals := fileHeader.Totals + fmt.Fprintf(outFile, "AccountTotals - Online Money: %d\nAccountTotals - Online RewardUnits : %d\nAccountTotals - Offline Money: %d\nAccountTotals - Offline RewardUnits : %d\nAccountTotals - Not Participating Money: %d\nAccountTotals - Not Participating Money RewardUnits: %d\nAccountTotals - Rewards Level: %d\n", + totals.Online.Money.Raw, totals.Online.RewardUnits, + totals.Offline.Money.Raw, totals.Offline.RewardUnits, + totals.NotParticipating.Money.Raw, totals.NotParticipating.RewardUnits, + totals.RewardsLevel) + } + return dbAccessor.Atomic(func(tx *sql.Tx) (err error) { + if fileHeader.Version == 0 { + var totals ledger.AccountTotals + id := "" + row := tx.QueryRow("SELECT online, onlinerewardunits, offline, offlinerewardunits, notparticipating, notparticipatingrewardunits, rewardslevel FROM accounttotals WHERE id=?", id) + err = row.Scan(&totals.Online.Money.Raw, &totals.Online.RewardUnits, + &totals.Offline.Money.Raw, &totals.Offline.RewardUnits, + &totals.NotParticipating.Money.Raw, &totals.NotParticipating.RewardUnits, + &totals.RewardsLevel) + if err != nil { + return err + } + fmt.Fprintf(outFile, "AccountTotals - Online Money: %d\nAccountTotals - Online RewardUnits : %d\nAccountTotals - Offline Money: %d\nAccountTotals - Offline RewardUnits : %d\nAccountTotals - Not Participating Money: %d\nAccountTotals - Not Participating Money RewardUnits: %d\nAccountTotals - Rewards Level: %d\n", + totals.Online.Money.Raw, totals.Online.RewardUnits, + totals.Offline.Money.Raw, totals.Offline.RewardUnits, + totals.NotParticipating.Money.Raw, totals.NotParticipating.RewardUnits, + totals.RewardsLevel) + } + + balancesTable := "accountbase" + if fileHeader.Version != 0 { + balancesTable = "catchpointbalances" + } + rows, err := tx.Query(fmt.Sprintf("SELECT address, data FROM %s order by address", balancesTable)) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var addrbuf []byte + var buf []byte + err = rows.Scan(&addrbuf, &buf) + if err != nil { + return + } + + var data basics.AccountData + err = protocol.Decode(buf, &data) + if err != nil { + return + } + + var addr basics.Address + if len(addrbuf) != len(addr) { + err = fmt.Errorf("Account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return + } + copy(addr[:], addrbuf) + jsonData, err := json.Marshal(data) + if err != nil { + return err + } + + fmt.Fprintf(outFile, "%v : %s\n", addr, string(jsonData)) + } + + err = rows.Err() + return nil + }) +} diff --git a/cmd/catchpointdump/net.go b/cmd/catchpointdump/net.go new file mode 100644 index 0000000000..b71c515b34 --- /dev/null +++ b/cmd/catchpointdump/net.go @@ -0,0 +1,160 @@ +// 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 . + +package main + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/logging" + "github.com/algorand/go-algorand/network" + tools "github.com/algorand/go-algorand/tools/network" +) + +var networkName string +var round int + +func init() { + netCmd.Flags().StringVarP(&networkName, "net", "n", "", "Specify the network name ( i.e. mainnet.algorand.network )") + netCmd.Flags().IntVarP(&round, "round", "r", 0, "Specify the round number ( i.e. 7700000 )") +} + +var netCmd = &cobra.Command{ + Use: "net", + Short: "Download and decode all the catchpoints files from all the relays on the network for a particular round", + Long: "Download and decode all the catchpoints files from all the relays on the network for a particular round", + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, args []string) { + if networkName == "" || round == 0 { + cmd.HelpFunc()(cmd, args) + return + } + + addrs, err := tools.ReadFromSRV("algobootstrap", "tcp", networkName, "", false) + if err != nil || len(addrs) == 0 { + reportErrorf("Unable to bootstrap records for '%s' : %v", networkName, err) + } + for _, addr := range addrs { + catchpointFileBytes, err := downloadCatchpoint(addr) + if err != nil || catchpointFileBytes == nil { + reportInfof("failed to download catchpoint from '%s' : %v", addr, err) + continue + } + err = saveCatchpointTarFile(addr, catchpointFileBytes) + if err != nil { + reportInfof("failed to save catchpoint file for '%s' : %v", addr, err) + continue + } + err = makeFileDump(addr, catchpointFileBytes) + if err != nil { + reportInfof("failed to make a dump from tar file for '%s' : %v", addr, err) + continue + } + } + }, +} + +func downloadCatchpoint(addr string) ([]byte, error) { + genesisID := strings.Split(networkName, ".")[0] + "-v1.0" + url := "http://" + addr + "/v1/" + genesisID + "/ledger/" + strconv.FormatUint(uint64(round), 36) + fmt.Printf("downloading from %s\n", url) + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + timeoutContext, timeoutContextCancel := context.WithTimeout(context.Background(), 40*time.Second) + defer timeoutContextCancel() + request = request.WithContext(timeoutContext) + network.SetUserAgentHeader(request.Header) + response, err := http.DefaultClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + // check to see that we had no errors. + switch response.StatusCode { + case http.StatusOK: + case http.StatusNotFound: // server could not find a block with that round numbers. + return nil, fmt.Errorf("no catchpoint file for round %d", round) + default: + return nil, fmt.Errorf("error response status code %d", response.StatusCode) + } + bytes, err := ioutil.ReadAll(response.Body) + return bytes, err +} + +func saveCatchpointTarFile(addr string, catchpointFileBytes []byte) error { + // make a directory: + dirName := "./" + strings.Split(networkName, ".")[0] + "/" + strings.Split(addr, ".")[0] + os.RemoveAll(dirName) + err := os.MkdirAll(dirName, 0777) + if err != nil && !os.IsExist(err) { + return err + } + err = ioutil.WriteFile(dirName+"/"+strconv.FormatUint(uint64(round), 10)+".tar", catchpointFileBytes, 0666) + return err +} + +func makeFileDump(addr string, catchpointFileBytes []byte) error { + genesisInitState := ledger.InitState{} + cfg := config.GetDefaultLocal() + l, err := ledger.OpenLedger(logging.Base(), "./ledger", false, genesisInitState, cfg) + if err != nil { + reportErrorf("Unable to open ledger : %v", err) + } + + defer os.Remove("./ledger.block.sqlite") + defer os.Remove("./ledger.block.sqlite-shm") + defer os.Remove("./ledger.block.sqlite-wal") + defer os.Remove("./ledger.tracker.sqlite") + defer os.Remove("./ledger.tracker.sqlite-shm") + defer os.Remove("./ledger.tracker.sqlite-wal") + defer l.Close() + + catchupAccessor := ledger.MakeCatchpointCatchupAccessor(l, logging.Base()) + err = catchupAccessor.ResetStagingBalances(context.Background(), true) + if err != nil { + reportErrorf("Unable to initialize catchup database : %v", err) + } + var fileHeader ledger.CatchpointFileHeader + fileHeader, err = loadCatchpointIntoDatabase(context.Background(), catchupAccessor, catchpointFileBytes) + if err != nil { + reportErrorf("Unable to load catchpoint file into in-memory database : %v", err) + } + + dirName := "./" + strings.Split(networkName, ".")[0] + "/" + strings.Split(addr, ".")[0] + outFile, err := os.OpenFile(dirName+"/"+strconv.FormatUint(uint64(round), 10)+".dump", os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return err + } + err = printAccountsDatabase("./ledger.tracker.sqlite", fileHeader, outFile) + if err != nil { + return err + } + return nil +} diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index ff06fd4606..392639eb8c 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -59,7 +59,7 @@ type catchpointWriter struct { headerWritten bool balancesOffset int balancesChunk catchpointFileBalancesChunk - fileHeader *catchpointFileHeader + fileHeader *CatchpointFileHeader balancesChunkNum uint64 writtenBytes int64 blocksRound basics.Round @@ -74,7 +74,9 @@ type encodedBalanceRecord struct { AccountData msgp.Raw `codec:"ad,allocbound=basics.MaxEncodedAccountDataSize"` } -type catchpointFileHeader struct { +// CatchpointFileHeader is the content we would have in the "content.msgpack" file in the catchpoint tar archive. +// we need it to be public, as it's being decoded externaly by the catchpointdump utility. +type CatchpointFileHeader struct { _struct struct{} `codec:",omitempty,omitemptyarray"` Version uint64 `codec:"version"` @@ -228,7 +230,7 @@ func (cw *catchpointWriter) readDatabaseStep(tx *sql.Tx) (err error) { } func (cw *catchpointWriter) readHeaderFromDatabase(tx *sql.Tx) (err error) { - var header catchpointFileHeader + var header CatchpointFileHeader header.BalancesRound, _, err = accountsRound(tx) if err != nil { return diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index de813e8b2c..ebb0202c12 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -195,7 +195,7 @@ func TestBasicCatchpointWriter(t *testing.T) { } } if header.Name == "content.msgpack" { - var fileHeader catchpointFileHeader + var fileHeader CatchpointFileHeader err = protocol.Decode(balancesBlockBytes, &fileHeader) require.NoError(t, err) require.Equal(t, catchpointLabel, fileHeader.Catchpoint) diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index c09f4ba4c5..288adbb823 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -234,7 +234,7 @@ func (c *CatchpointCatchupAccessorImpl) processStagingContent(ctx context.Contex if progress.SeenHeader { return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: content chunk already seen") } - var fileHeader catchpointFileHeader + var fileHeader CatchpointFileHeader err = protocol.Decode(bytes, &fileHeader) if err != nil { return err diff --git a/ledger/msgp_gen.go b/ledger/msgp_gen.go index e0def7af95..72ea2396f7 100644 --- a/ledger/msgp_gen.go +++ b/ledger/msgp_gen.go @@ -31,6 +31,14 @@ import ( // |-----> Msgsize // |-----> MsgIsZero // +// CatchpointFileHeader +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// // catchpointFileBalancesChunk // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -39,14 +47,6 @@ import ( // |-----> (*) Msgsize // |-----> (*) MsgIsZero // -// catchpointFileHeader -// |-----> (*) MarshalMsg -// |-----> (*) CanMarshalMsg -// |-----> (*) UnmarshalMsg -// |-----> (*) CanUnmarshalMsg -// |-----> (*) Msgsize -// |-----> (*) MsgIsZero -// // catchpointState // |-----> MarshalMsg // |-----> CanMarshalMsg @@ -881,177 +881,190 @@ func (z CatchpointCatchupState) MsgIsZero() bool { } // MarshalMsg implements msgp.Marshaler -func (z *catchpointFileBalancesChunk) MarshalMsg(b []byte) (o []byte, err error) { +func (z *CatchpointFileHeader) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0002Len := uint32(1) - var zb0002Mask uint8 /* 2 bits */ - if len((*z).Balances) == 0 { - zb0002Len-- - zb0002Mask |= 0x2 + zb0001Len := uint32(8) + var zb0001Mask uint16 /* 9 bits */ + if (*z).Totals.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x2 } - // variable map header, size zb0002Len - o = append(o, 0x80|uint8(zb0002Len)) - if zb0002Len != 0 { - if (zb0002Mask & 0x2) == 0 { // if not empty - // string "bl" - o = append(o, 0xa2, 0x62, 0x6c) - if (*z).Balances == nil { - o = msgp.AppendNil(o) - } else { - o = msgp.AppendArrayHeader(o, uint32(len((*z).Balances))) + if (*z).TotalAccounts == 0 { + zb0001Len-- + zb0001Mask |= 0x4 + } + if (*z).BalancesRound.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x8 + } + if (*z).BlockHeaderDigest.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).BlocksRound.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } + if (*z).Catchpoint == "" { + zb0001Len-- + zb0001Mask |= 0x40 + } + if (*z).TotalChunks == 0 { + zb0001Len-- + zb0001Mask |= 0x80 + } + if (*z).Version == 0 { + zb0001Len-- + zb0001Mask |= 0x100 + } + // variable map header, size zb0001Len + o = append(o, 0x80|uint8(zb0001Len)) + if zb0001Len != 0 { + if (zb0001Mask & 0x2) == 0 { // if not empty + // string "accountTotals" + o = append(o, 0xad, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73) + o, err = (*z).Totals.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Totals") + return } - for zb0001 := range (*z).Balances { - // omitempty: check for empty values - zb0003Len := uint32(2) - var zb0003Mask uint8 /* 3 bits */ - if (*z).Balances[zb0001].AccountData.MsgIsZero() { - zb0003Len-- - zb0003Mask |= 0x2 - } - if (*z).Balances[zb0001].Address.MsgIsZero() { - zb0003Len-- - zb0003Mask |= 0x4 - } - // variable map header, size zb0003Len - o = append(o, 0x80|uint8(zb0003Len)) - if (zb0003Mask & 0x2) == 0 { // if not empty - // string "ad" - o = append(o, 0xa2, 0x61, 0x64) - o, err = (*z).Balances[zb0001].AccountData.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "AccountData") - return - } - } - if (zb0003Mask & 0x4) == 0 { // if not empty - // string "pk" - o = append(o, 0xa2, 0x70, 0x6b) - o, err = (*z).Balances[zb0001].Address.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "Address") - return - } - } + } + if (zb0001Mask & 0x4) == 0 { // if not empty + // string "accountsCount" + o = append(o, 0xad, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) + o = msgp.AppendUint64(o, (*z).TotalAccounts) + } + if (zb0001Mask & 0x8) == 0 { // if not empty + // string "balancesRound" + o = append(o, 0xad, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x6f, 0x75, 0x6e, 0x64) + o, err = (*z).BalancesRound.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "BalancesRound") + return + } + } + if (zb0001Mask & 0x10) == 0 { // if not empty + // string "blockHeaderDigest" + o = append(o, 0xb1, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74) + o, err = (*z).BlockHeaderDigest.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "BlockHeaderDigest") + return + } + } + if (zb0001Mask & 0x20) == 0 { // if not empty + // string "blocksRound" + o = append(o, 0xab, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x6f, 0x75, 0x6e, 0x64) + o, err = (*z).BlocksRound.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "BlocksRound") + return } } + if (zb0001Mask & 0x40) == 0 { // if not empty + // string "catchpoint" + o = append(o, 0xaa, 0x63, 0x61, 0x74, 0x63, 0x68, 0x70, 0x6f, 0x69, 0x6e, 0x74) + o = msgp.AppendString(o, (*z).Catchpoint) + } + if (zb0001Mask & 0x80) == 0 { // if not empty + // string "chunksCount" + o = append(o, 0xab, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) + o = msgp.AppendUint64(o, (*z).TotalChunks) + } + if (zb0001Mask & 0x100) == 0 { // if not empty + // string "version" + o = append(o, 0xa7, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) + o = msgp.AppendUint64(o, (*z).Version) + } } return } -func (_ *catchpointFileBalancesChunk) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileBalancesChunk) +func (_ *CatchpointFileHeader) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*CatchpointFileHeader) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointFileBalancesChunk) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *CatchpointFileHeader) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0002 int - var zb0003 bool - zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0001 int + var zb0002 bool + zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0002 > 0 { - zb0002-- - var zb0004 int - var zb0005 bool - zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0001 > 0 { + zb0001-- + (*z).Version, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances") + err = msgp.WrapError(err, "struct-from-array", "Version") return } - if zb0004 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0004), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "struct-from-array", "Balances") + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BalancesRound.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "BalancesRound") return } - if zb0005 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0004 { - (*z).Balances = ((*z).Balances)[:zb0004] - } else { - (*z).Balances = make([]encodedBalanceRecord, zb0004) + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BlocksRound.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "BlocksRound") + return } - for zb0001 := range (*z).Balances { - var zb0006 int - var zb0007 bool - zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - if zb0006 > 0 { - zb0006-- - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array", "Address") - return - } - } - if zb0006 > 0 { - zb0006-- - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array", "AccountData") - return - } - } - if zb0006 > 0 { - err = msgp.ErrTooManyArrayFields(zb0006) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - if zb0007 { - (*z).Balances[zb0001] = encodedBalanceRecord{} - } - for zb0006 > 0 { - zb0006-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - switch string(field) { - case "pk": - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "Address") - return - } - case "ad": - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "AccountData") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) - return - } - } - } - } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).Totals.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Totals") + return } } - if zb0002 > 0 { - err = msgp.ErrTooManyArrayFields(zb0002) + if zb0001 > 0 { + zb0001-- + (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalAccounts") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "TotalChunks") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).Catchpoint, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Catchpoint") + return + } + } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).BlockHeaderDigest.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "BlockHeaderDigest") + return + } + } + if zb0001 > 0 { + err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -1062,107 +1075,64 @@ func (z *catchpointFileBalancesChunk) UnmarshalMsg(bts []byte) (o []byte, err er err = msgp.WrapError(err) return } - if zb0003 { - (*z) = catchpointFileBalancesChunk{} + if zb0002 { + (*z) = CatchpointFileHeader{} } - for zb0002 > 0 { - zb0002-- + for zb0001 > 0 { + zb0001-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) return } switch string(field) { - case "bl": - var zb0008 int - var zb0009 bool - zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) + case "version": + (*z).Version, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { - err = msgp.WrapError(err, "Balances") + err = msgp.WrapError(err, "Version") return } - if zb0008 > BalancesPerCatchpointFileChunk { - err = msgp.ErrOverflow(uint64(zb0008), uint64(BalancesPerCatchpointFileChunk)) - err = msgp.WrapError(err, "Balances") + case "balancesRound": + bts, err = (*z).BalancesRound.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "BalancesRound") return } - if zb0009 { - (*z).Balances = nil - } else if (*z).Balances != nil && cap((*z).Balances) >= zb0008 { - (*z).Balances = ((*z).Balances)[:zb0008] - } else { - (*z).Balances = make([]encodedBalanceRecord, zb0008) + case "blocksRound": + bts, err = (*z).BlocksRound.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "BlocksRound") + return } - for zb0001 := range (*z).Balances { - var zb0010 int - var zb0011 bool - zb0010, zb0011, bts, err = msgp.ReadMapHeaderBytes(bts) - if _, ok := err.(msgp.TypeError); ok { - zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) - return - } - if zb0010 > 0 { - zb0010-- - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "Address") - return - } - } - if zb0010 > 0 { - zb0010-- - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "AccountData") - return - } - } - if zb0010 > 0 { - err = msgp.ErrTooManyArrayFields(zb0010) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array") - return - } - } - } else { - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) - return - } - if zb0011 { - (*z).Balances[zb0001] = encodedBalanceRecord{} - } - for zb0010 > 0 { - zb0010-- - field, bts, err = msgp.ReadMapKeyZC(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) - return - } - switch string(field) { - case "pk": - bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "Address") - return - } - case "ad": - bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001, "AccountData") - return - } - default: - err = msgp.ErrNoField(string(field)) - if err != nil { - err = msgp.WrapError(err, "Balances", zb0001) - return - } - } - } - } + case "accountTotals": + bts, err = (*z).Totals.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Totals") + return + } + case "accountsCount": + (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalAccounts") + return + } + case "chunksCount": + (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "TotalChunks") + return + } + case "catchpoint": + (*z).Catchpoint, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Catchpoint") + return + } + case "blockHeaderDigest": + bts, err = (*z).BlockHeaderDigest.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "BlockHeaderDigest") + return } default: err = msgp.ErrNoField(string(field)) @@ -1177,210 +1147,194 @@ func (z *catchpointFileBalancesChunk) UnmarshalMsg(bts []byte) (o []byte, err er return } -func (_ *catchpointFileBalancesChunk) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileBalancesChunk) +func (_ *CatchpointFileHeader) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*CatchpointFileHeader) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *catchpointFileBalancesChunk) Msgsize() (s int) { - s = 1 + 3 + msgp.ArrayHeaderSize - for zb0001 := range (*z).Balances { - s += 1 + 3 + (*z).Balances[zb0001].Address.Msgsize() + 3 + (*z).Balances[zb0001].AccountData.Msgsize() - } +func (z *CatchpointFileHeader) Msgsize() (s int) { + s = 1 + 8 + msgp.Uint64Size + 14 + (*z).BalancesRound.Msgsize() + 12 + (*z).BlocksRound.Msgsize() + 14 + (*z).Totals.Msgsize() + 14 + msgp.Uint64Size + 12 + msgp.Uint64Size + 11 + msgp.StringPrefixSize + len((*z).Catchpoint) + 18 + (*z).BlockHeaderDigest.Msgsize() return } // MsgIsZero returns whether this is a zero value -func (z *catchpointFileBalancesChunk) MsgIsZero() bool { - return (len((*z).Balances) == 0) +func (z *CatchpointFileHeader) MsgIsZero() bool { + return ((*z).Version == 0) && ((*z).BalancesRound.MsgIsZero()) && ((*z).BlocksRound.MsgIsZero()) && ((*z).Totals.MsgIsZero()) && ((*z).TotalAccounts == 0) && ((*z).TotalChunks == 0) && ((*z).Catchpoint == "") && ((*z).BlockHeaderDigest.MsgIsZero()) } // MarshalMsg implements msgp.Marshaler -func (z *catchpointFileHeader) MarshalMsg(b []byte) (o []byte, err error) { +func (z *catchpointFileBalancesChunk) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(8) - var zb0001Mask uint16 /* 9 bits */ - if (*z).Totals.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x2 - } - if (*z).TotalAccounts == 0 { - zb0001Len-- - zb0001Mask |= 0x4 - } - if (*z).BalancesRound.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x8 - } - if (*z).BlockHeaderDigest.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x10 - } - if (*z).BlocksRound.MsgIsZero() { - zb0001Len-- - zb0001Mask |= 0x20 - } - if (*z).Catchpoint == "" { - zb0001Len-- - zb0001Mask |= 0x40 - } - if (*z).TotalChunks == 0 { - zb0001Len-- - zb0001Mask |= 0x80 - } - if (*z).Version == 0 { - zb0001Len-- - zb0001Mask |= 0x100 + zb0002Len := uint32(1) + var zb0002Mask uint8 /* 2 bits */ + if len((*z).Balances) == 0 { + zb0002Len-- + zb0002Mask |= 0x2 } - // variable map header, size zb0001Len - o = append(o, 0x80|uint8(zb0001Len)) - if zb0001Len != 0 { - if (zb0001Mask & 0x2) == 0 { // if not empty - // string "accountTotals" - o = append(o, 0xad, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x73) - o, err = (*z).Totals.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "Totals") - return - } - } - if (zb0001Mask & 0x4) == 0 { // if not empty - // string "accountsCount" - o = append(o, 0xad, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) - o = msgp.AppendUint64(o, (*z).TotalAccounts) - } - if (zb0001Mask & 0x8) == 0 { // if not empty - // string "balancesRound" - o = append(o, 0xad, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x6f, 0x75, 0x6e, 0x64) - o, err = (*z).BalancesRound.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "BalancesRound") - return - } - } - if (zb0001Mask & 0x10) == 0 { // if not empty - // string "blockHeaderDigest" - o = append(o, 0xb1, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74) - o, err = (*z).BlockHeaderDigest.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "BlockHeaderDigest") - return + // variable map header, size zb0002Len + o = append(o, 0x80|uint8(zb0002Len)) + if zb0002Len != 0 { + if (zb0002Mask & 0x2) == 0 { // if not empty + // string "bl" + o = append(o, 0xa2, 0x62, 0x6c) + if (*z).Balances == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).Balances))) } - } - if (zb0001Mask & 0x20) == 0 { // if not empty - // string "blocksRound" - o = append(o, 0xab, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x52, 0x6f, 0x75, 0x6e, 0x64) - o, err = (*z).BlocksRound.MarshalMsg(o) - if err != nil { - err = msgp.WrapError(err, "BlocksRound") - return + for zb0001 := range (*z).Balances { + // omitempty: check for empty values + zb0003Len := uint32(2) + var zb0003Mask uint8 /* 3 bits */ + if (*z).Balances[zb0001].AccountData.MsgIsZero() { + zb0003Len-- + zb0003Mask |= 0x2 + } + if (*z).Balances[zb0001].Address.MsgIsZero() { + zb0003Len-- + zb0003Mask |= 0x4 + } + // variable map header, size zb0003Len + o = append(o, 0x80|uint8(zb0003Len)) + if (zb0003Mask & 0x2) == 0 { // if not empty + // string "ad" + o = append(o, 0xa2, 0x61, 0x64) + o, err = (*z).Balances[zb0001].AccountData.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "AccountData") + return + } + } + if (zb0003Mask & 0x4) == 0 { // if not empty + // string "pk" + o = append(o, 0xa2, 0x70, 0x6b) + o, err = (*z).Balances[zb0001].Address.MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "Address") + return + } + } } } - if (zb0001Mask & 0x40) == 0 { // if not empty - // string "catchpoint" - o = append(o, 0xaa, 0x63, 0x61, 0x74, 0x63, 0x68, 0x70, 0x6f, 0x69, 0x6e, 0x74) - o = msgp.AppendString(o, (*z).Catchpoint) - } - if (zb0001Mask & 0x80) == 0 { // if not empty - // string "chunksCount" - o = append(o, 0xab, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x43, 0x6f, 0x75, 0x6e, 0x74) - o = msgp.AppendUint64(o, (*z).TotalChunks) - } - if (zb0001Mask & 0x100) == 0 { // if not empty - // string "version" - o = append(o, 0xa7, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e) - o = msgp.AppendUint64(o, (*z).Version) - } } return } -func (_ *catchpointFileHeader) CanMarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileHeader) +func (_ *catchpointFileBalancesChunk) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileBalancesChunk) return ok } // UnmarshalMsg implements msgp.Unmarshaler -func (z *catchpointFileHeader) UnmarshalMsg(bts []byte) (o []byte, err error) { +func (z *catchpointFileBalancesChunk) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0001 int - var zb0002 bool - zb0001, zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0002 int + var zb0003 bool + zb0002, zb0003, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0001, zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0002, zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0001 > 0 { - zb0001-- - (*z).Version, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Version") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).BalancesRound.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "BalancesRound") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).BlocksRound.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "BlocksRound") - return - } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).Totals.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Totals") - return - } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) + if zb0002 > 0 { + zb0002-- + var zb0004 int + var zb0005 bool + zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalAccounts") + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "TotalChunks") + if zb0004 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0004), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "struct-from-array", "Balances") return } - } - if zb0001 > 0 { - zb0001-- - (*z).Catchpoint, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "Catchpoint") - return + if zb0005 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0004 { + (*z).Balances = ((*z).Balances)[:zb0004] + } else { + (*z).Balances = make([]encodedBalanceRecord, zb0004) } - } - if zb0001 > 0 { - zb0001-- - bts, err = (*z).BlockHeaderDigest.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "struct-from-array", "BlockHeaderDigest") - return + for zb0001 := range (*z).Balances { + var zb0006 int + var zb0007 bool + zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + return + } + if zb0006 > 0 { + zb0006-- + bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array", "Address") + return + } + } + if zb0006 > 0 { + zb0006-- + bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array", "AccountData") + return + } + } + if zb0006 > 0 { + err = msgp.ErrTooManyArrayFields(zb0006) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + return + } + if zb0007 { + (*z).Balances[zb0001] = encodedBalanceRecord{} + } + for zb0006 > 0 { + zb0006-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + return + } + switch string(field) { + case "pk": + bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "Address") + return + } + case "ad": + bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001, "AccountData") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Balances", zb0001) + return + } + } + } + } } } - if zb0001 > 0 { - err = msgp.ErrTooManyArrayFields(zb0001) + if zb0002 > 0 { + err = msgp.ErrTooManyArrayFields(zb0002) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -1391,64 +1345,107 @@ func (z *catchpointFileHeader) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0002 { - (*z) = catchpointFileHeader{} + if zb0003 { + (*z) = catchpointFileBalancesChunk{} } - for zb0001 > 0 { - zb0001-- + for zb0002 > 0 { + zb0002-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) return } switch string(field) { - case "version": - (*z).Version, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "Version") - return - } - case "balancesRound": - bts, err = (*z).BalancesRound.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "BalancesRound") - return - } - case "blocksRound": - bts, err = (*z).BlocksRound.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "BlocksRound") - return - } - case "accountTotals": - bts, err = (*z).Totals.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "Totals") - return - } - case "accountsCount": - (*z).TotalAccounts, bts, err = msgp.ReadUint64Bytes(bts) + case "bl": + var zb0008 int + var zb0009 bool + zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { - err = msgp.WrapError(err, "TotalAccounts") + err = msgp.WrapError(err, "Balances") return } - case "chunksCount": - (*z).TotalChunks, bts, err = msgp.ReadUint64Bytes(bts) - if err != nil { - err = msgp.WrapError(err, "TotalChunks") + if zb0008 > BalancesPerCatchpointFileChunk { + err = msgp.ErrOverflow(uint64(zb0008), uint64(BalancesPerCatchpointFileChunk)) + err = msgp.WrapError(err, "Balances") return } - case "catchpoint": - (*z).Catchpoint, bts, err = msgp.ReadStringBytes(bts) - if err != nil { - err = msgp.WrapError(err, "Catchpoint") - return + if zb0009 { + (*z).Balances = nil + } else if (*z).Balances != nil && cap((*z).Balances) >= zb0008 { + (*z).Balances = ((*z).Balances)[:zb0008] + } else { + (*z).Balances = make([]encodedBalanceRecord, zb0008) } - case "blockHeaderDigest": - bts, err = (*z).BlockHeaderDigest.UnmarshalMsg(bts) - if err != nil { - err = msgp.WrapError(err, "BlockHeaderDigest") - return + for zb0001 := range (*z).Balances { + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001) + return + } + if zb0010 > 0 { + zb0010-- + bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "Address") + return + } + } + if zb0010 > 0 { + zb0010-- + bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array", "AccountData") + return + } + } + if zb0010 > 0 { + err = msgp.ErrTooManyArrayFields(zb0010) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001) + return + } + if zb0011 { + (*z).Balances[zb0001] = encodedBalanceRecord{} + } + for zb0010 > 0 { + zb0010-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001) + return + } + switch string(field) { + case "pk": + bts, err = (*z).Balances[zb0001].Address.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "Address") + return + } + case "ad": + bts, err = (*z).Balances[zb0001].AccountData.UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001, "AccountData") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err, "Balances", zb0001) + return + } + } + } + } } default: err = msgp.ErrNoField(string(field)) @@ -1463,20 +1460,23 @@ func (z *catchpointFileHeader) UnmarshalMsg(bts []byte) (o []byte, err error) { return } -func (_ *catchpointFileHeader) CanUnmarshalMsg(z interface{}) bool { - _, ok := (z).(*catchpointFileHeader) +func (_ *catchpointFileBalancesChunk) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*catchpointFileBalancesChunk) return ok } // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message -func (z *catchpointFileHeader) Msgsize() (s int) { - s = 1 + 8 + msgp.Uint64Size + 14 + (*z).BalancesRound.Msgsize() + 12 + (*z).BlocksRound.Msgsize() + 14 + (*z).Totals.Msgsize() + 14 + msgp.Uint64Size + 12 + msgp.Uint64Size + 11 + msgp.StringPrefixSize + len((*z).Catchpoint) + 18 + (*z).BlockHeaderDigest.Msgsize() +func (z *catchpointFileBalancesChunk) Msgsize() (s int) { + s = 1 + 3 + msgp.ArrayHeaderSize + for zb0001 := range (*z).Balances { + s += 1 + 3 + (*z).Balances[zb0001].Address.Msgsize() + 3 + (*z).Balances[zb0001].AccountData.Msgsize() + } return } // MsgIsZero returns whether this is a zero value -func (z *catchpointFileHeader) MsgIsZero() bool { - return ((*z).Version == 0) && ((*z).BalancesRound.MsgIsZero()) && ((*z).BlocksRound.MsgIsZero()) && ((*z).Totals.MsgIsZero()) && ((*z).TotalAccounts == 0) && ((*z).TotalChunks == 0) && ((*z).Catchpoint == "") && ((*z).BlockHeaderDigest.MsgIsZero()) +func (z *catchpointFileBalancesChunk) MsgIsZero() bool { + return (len((*z).Balances) == 0) } // MarshalMsg implements msgp.Marshaler diff --git a/ledger/msgp_gen_test.go b/ledger/msgp_gen_test.go index 63e2b8be22..89f19244f5 100644 --- a/ledger/msgp_gen_test.go +++ b/ledger/msgp_gen_test.go @@ -135,8 +135,8 @@ func BenchmarkUnmarshalAlgoCount(b *testing.B) { } } -func TestMarshalUnmarshalcatchpointFileBalancesChunk(t *testing.T) { - v := catchpointFileBalancesChunk{} +func TestMarshalUnmarshalCatchpointFileHeader(t *testing.T) { + v := CatchpointFileHeader{} bts, err := v.MarshalMsg(nil) if err != nil { t.Fatal(err) @@ -158,12 +158,12 @@ func TestMarshalUnmarshalcatchpointFileBalancesChunk(t *testing.T) { } } -func TestRandomizedEncodingcatchpointFileBalancesChunk(t *testing.T) { - protocol.RunEncodingTest(t, &catchpointFileBalancesChunk{}) +func TestRandomizedEncodingCatchpointFileHeader(t *testing.T) { + protocol.RunEncodingTest(t, &CatchpointFileHeader{}) } -func BenchmarkMarshalMsgcatchpointFileBalancesChunk(b *testing.B) { - v := catchpointFileBalancesChunk{} +func BenchmarkMarshalMsgCatchpointFileHeader(b *testing.B) { + v := CatchpointFileHeader{} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -171,8 +171,8 @@ func BenchmarkMarshalMsgcatchpointFileBalancesChunk(b *testing.B) { } } -func BenchmarkAppendMsgcatchpointFileBalancesChunk(b *testing.B) { - v := catchpointFileBalancesChunk{} +func BenchmarkAppendMsgCatchpointFileHeader(b *testing.B) { + v := CatchpointFileHeader{} bts := make([]byte, 0, v.Msgsize()) bts, _ = v.MarshalMsg(bts[0:0]) b.SetBytes(int64(len(bts))) @@ -183,8 +183,8 @@ func BenchmarkAppendMsgcatchpointFileBalancesChunk(b *testing.B) { } } -func BenchmarkUnmarshalcatchpointFileBalancesChunk(b *testing.B) { - v := catchpointFileBalancesChunk{} +func BenchmarkUnmarshalCatchpointFileHeader(b *testing.B) { + v := CatchpointFileHeader{} bts, _ := v.MarshalMsg(nil) b.ReportAllocs() b.SetBytes(int64(len(bts))) @@ -197,8 +197,8 @@ func BenchmarkUnmarshalcatchpointFileBalancesChunk(b *testing.B) { } } -func TestMarshalUnmarshalcatchpointFileHeader(t *testing.T) { - v := catchpointFileHeader{} +func TestMarshalUnmarshalcatchpointFileBalancesChunk(t *testing.T) { + v := catchpointFileBalancesChunk{} bts, err := v.MarshalMsg(nil) if err != nil { t.Fatal(err) @@ -220,12 +220,12 @@ func TestMarshalUnmarshalcatchpointFileHeader(t *testing.T) { } } -func TestRandomizedEncodingcatchpointFileHeader(t *testing.T) { - protocol.RunEncodingTest(t, &catchpointFileHeader{}) +func TestRandomizedEncodingcatchpointFileBalancesChunk(t *testing.T) { + protocol.RunEncodingTest(t, &catchpointFileBalancesChunk{}) } -func BenchmarkMarshalMsgcatchpointFileHeader(b *testing.B) { - v := catchpointFileHeader{} +func BenchmarkMarshalMsgcatchpointFileBalancesChunk(b *testing.B) { + v := catchpointFileBalancesChunk{} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -233,8 +233,8 @@ func BenchmarkMarshalMsgcatchpointFileHeader(b *testing.B) { } } -func BenchmarkAppendMsgcatchpointFileHeader(b *testing.B) { - v := catchpointFileHeader{} +func BenchmarkAppendMsgcatchpointFileBalancesChunk(b *testing.B) { + v := catchpointFileBalancesChunk{} bts := make([]byte, 0, v.Msgsize()) bts, _ = v.MarshalMsg(bts[0:0]) b.SetBytes(int64(len(bts))) @@ -245,8 +245,8 @@ func BenchmarkAppendMsgcatchpointFileHeader(b *testing.B) { } } -func BenchmarkUnmarshalcatchpointFileHeader(b *testing.B) { - v := catchpointFileHeader{} +func BenchmarkUnmarshalcatchpointFileBalancesChunk(b *testing.B) { + v := catchpointFileBalancesChunk{} bts, _ := v.MarshalMsg(nil) b.ReportAllocs() b.SetBytes(int64(len(bts))) From 627f1089e1d3e94af2c52fe221b2e0ac6e30099c Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 13 Jul 2020 14:22:46 -0400 Subject: [PATCH 097/267] Fix a data race in Basic Catchpoint Catchup Test Fix a data race in Basic Catchpoint Catchup Test --- .../catchup/catchpointCatchup_test.go | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/test/e2e-go/features/catchup/catchpointCatchup_test.go b/test/e2e-go/features/catchup/catchpointCatchup_test.go index c80518a37b..3ade3d372e 100644 --- a/test/e2e-go/features/catchup/catchpointCatchup_test.go +++ b/test/e2e-go/features/catchup/catchpointCatchup_test.go @@ -17,6 +17,7 @@ package catchup import ( + "fmt" "net/http" "os" "os/exec" @@ -26,6 +27,7 @@ import ( "testing" "time" + "github.com/algorand/go-deadlock" "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" @@ -36,20 +38,45 @@ import ( "github.com/algorand/go-algorand/test/framework/fixtures" ) -func nodeExitWithError(t *testing.T, nc *nodecontrol.NodeController, err error) { +type nodeExitErrorCollector struct { + errors []error + messages []string + mu deadlock.Mutex + t *testing.T +} + +func (ec *nodeExitErrorCollector) nodeExitWithError(nc *nodecontrol.NodeController, err error) { if err == nil { return } exitError, ok := err.(*exec.ExitError) if !ok { - require.NoError(t, err, "Node at %s has terminated with an error", nc.GetDataDir()) + if err != nil { + ec.mu.Lock() + ec.errors = append(ec.errors, err) + ec.messages = append(ec.messages, "Node at %s has terminated with an error", nc.GetDataDir()) + ec.mu.Unlock() + } return } ws := exitError.Sys().(syscall.WaitStatus) exitCode := ws.ExitStatus() - require.NoError(t, err, "Node at %s has terminated with error code %d", nc.GetDataDir(), exitCode) + if err != nil { + ec.mu.Lock() + ec.errors = append(ec.errors, err) + ec.messages = append(ec.messages, fmt.Sprintf("Node at %s has terminated with error code %d", nc.GetDataDir(), exitCode)) + ec.mu.Unlock() + } +} + +func (ec *nodeExitErrorCollector) Print() { + ec.mu.Lock() + defer ec.mu.Unlock() + for i, err := range ec.errors { + require.NoError(ec.t, err, ec.messages[i]) + } } func TestBasicCatchpointCatchup(t *testing.T) { @@ -87,11 +114,14 @@ func TestBasicCatchpointCatchup(t *testing.T) { var fixture fixtures.RestClientFixture fixture.SetConsensus(consensus) + + errorsCollector := nodeExitErrorCollector{t: t} + defer errorsCollector.Print() + // Give the second node (which starts up last) all the stake so that its proposal always has better credentials, // and so that its proposal isn't dropped. Otherwise the test burns 17s to recover. We don't care about stake // distribution for catchup so this is fine. fixture.SetupNoStart(t, filepath.Join("nettemplates", "CatchpointCatchupTestNetwork.json")) - //defer fixture.Shutdown() // Get primary node primaryNode, err := fixture.GetNodeController("Primary") @@ -118,7 +148,7 @@ func TestBasicCatchpointCatchup(t *testing.T) { RedirectOutput: true, RunUnderHost: false, TelemetryOverride: "", - ExitErrorCallback: func(nc *nodecontrol.NodeController, err error) { nodeExitWithError(t, nc, err) }, + ExitErrorCallback: errorsCollector.nodeExitWithError, }) a.NoError(err) @@ -160,7 +190,7 @@ func TestBasicCatchpointCatchup(t *testing.T) { RedirectOutput: true, RunUnderHost: false, TelemetryOverride: "", - ExitErrorCallback: func(nc *nodecontrol.NodeController, err error) { nodeExitWithError(t, nc, err) }, + ExitErrorCallback: errorsCollector.nodeExitWithError, }) a.NoError(err) From 2c1a18c041d9a95e2d42d687cad345ac5f251e05 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 13 Jul 2020 16:15:36 -0400 Subject: [PATCH 098/267] fix merge error. --- ledger/ledger_perf_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/ledger_perf_test.go b/ledger/ledger_perf_test.go index cc87acd01f..c25e6e5c12 100644 --- a/ledger/ledger_perf_test.go +++ b/ledger/ledger_perf_test.go @@ -322,7 +322,7 @@ func benchmarkFullBlocks(params testParams, b *testing.B) { vc := alwaysVerifiedCache{} b.ResetTimer() for _, blk := range blocks { - _, err = l1.eval(context.Background(), blk, true, &vc, nil) + _, err = eval(context.Background(), l1, blk, true, &vc, nil) require.NoError(b, err) err = l1.AddBlock(blk, cert) require.NoError(b, err) From aebd7546630a1794abdfffed903d44925e042aef Mon Sep 17 00:00:00 2001 From: Derek Leung Date: Mon, 13 Jul 2020 17:54:45 -0400 Subject: [PATCH 099/267] Add goal app info for inspecting app properties. (#1239) goal app info is like goal asset info, printing out properties describing an app. --- cmd/goal/application.go | 39 +++++++++++++++++++++++++ test/scripts/e2e_subs/e2e-app-simple.sh | 34 +++++++++++++++++++-- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/cmd/goal/application.go b/cmd/goal/application.go index 692954612e..c0ca7dc776 100644 --- a/cmd/goal/application.go +++ b/cmd/goal/application.go @@ -77,6 +77,7 @@ func init() { appCmd.AddCommand(closeOutAppCmd) appCmd.AddCommand(clearAppCmd) appCmd.AddCommand(readStateAppCmd) + appCmd.AddCommand(infoAppCmd) appCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") appCmd.PersistentFlags().StringSliceVar(&appArgs, "app-arg", nil, "Args to encode for application transactions (all will be encoded to a byte slice). For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.") @@ -115,6 +116,7 @@ func init() { deleteAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") readStateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") updateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") + infoAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") // Add common transaction flags to all txn-generating app commands addTxnFlags(createAppCmd) @@ -154,6 +156,8 @@ func init() { updateAppCmd.MarkFlagRequired("from") readStateAppCmd.MarkFlagRequired("app-id") + + infoAppCmd.MarkFlagRequired("app-id") } type appCallArg struct { @@ -1026,3 +1030,38 @@ var readStateAppCmd = &cobra.Command{ return }, } + +var infoAppCmd = &cobra.Command{ + Use: "info", + Short: "Look up current parameters for an application", + Long: `Look up application information stored on the network, such as program hash.`, + Args: validateNoPosArgsFn, + Run: func(cmd *cobra.Command, _ []string) { + dataDir := ensureSingleDataDir() + client := ensureFullClient(dataDir) + + meta, err := client.ApplicationInformation(appIdx) + if err != nil { + reportErrorf(errorRequestFail, err) + } + params := meta.Params + + gsch := params.GlobalStateSchema + lsch := params.LocalStateSchema + + fmt.Printf("Application ID: %d\n", appIdx) + fmt.Printf("Creator: %v\n", params.Creator) + fmt.Printf("Approval hash: %v\n", basics.Address(logic.HashProgram(params.ApprovalProgram))) + fmt.Printf("Clear hash: %v\n", basics.Address(logic.HashProgram(params.ClearStateProgram))) + + if gsch != nil { + fmt.Printf("Max global byteslices: %d\n", gsch.NumByteSlice) + fmt.Printf("Max global integers: %d\n", gsch.NumUint) + } + + if lsch != nil { + fmt.Printf("Max local byteslices: %d\n", lsch.NumByteSlice) + fmt.Printf("Max local integers: %d\n", lsch.NumUint) + } + }, +} diff --git a/test/scripts/e2e_subs/e2e-app-simple.sh b/test/scripts/e2e_subs/e2e-app-simple.sh index adb96f4064..80e8e9add9 100755 --- a/test/scripts/e2e_subs/e2e-app-simple.sh +++ b/test/scripts/e2e_subs/e2e-app-simple.sh @@ -12,12 +12,42 @@ WALLET=$1 gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') +GLOBAL_INTS=2 + +echo 'int 1' > "${TEMPDIR}/simple.teal" +PROGRAM=($(${gcmd} clerk compile "${TEMPDIR}/simple.teal")) # Succeed in creating app that approves all transactions -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 1') --clear-prog <(echo 'int 1') --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 | grep Created | awk '{ print $6 }') +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${TEMPDIR}/simple.teal" --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints ${GLOBAL_INTS} --local-byteslices 0 --local-ints 0 | grep Created | awk '{ print $6 }') + +# Check that parameters were set correctly +APPID_CHECK=($(${gcmd} app info --app-id $APPID | grep "ID")) +CREATOR_CHECK=($(${gcmd} app info --app-id $APPID | grep "Creator")) +GLOBAL_CHECK=($(${gcmd} app info --app-id $APPID | grep "global integers")) +PROGRAM_CHECK=($(${gcmd} app info --app-id $APPID | grep "Approval")) + +if [[ ${APPID} != ${APPID_CHECK[2]} ]]; then + date '+app-create-test FAIL returned app ID does not match ${APPID} != ${APPID_CHECK[2]} %Y%m%d_%H%M%S' + false +fi + +if [[ ${ACCOUNT} != ${CREATOR_CHECK[1]} ]]; then + date '+app-create-test FAIL returned creator does not match ${ACCOUNT} != ${CREATOR_CHECK[1]} %Y%m%d_%H%M%S' + false +fi + +if [[ ${GLOBAL_INTS} != ${GLOBAL_CHECK[3]} ]]; then + date '+app-create-test FAIL returned global integers does not match ${GLOBAL_CHECK[3]} != ${GLOBAL_INTS} %Y%m%d_%H%M%S' + false +fi + +if [[ ${PROGRAM[1]} != ${PROGRAM_CHECK[2]} ]]; then + date '+app-create-test FAIL returned app ID does not match ${PROGRAM[1]} != ${PROGRAM_CHECK[2]} %Y%m%d_%H%M%S' + false +fi # Fail to create app if approval program rejects creation -RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 0') --clear-prog <(echo 'int 1') --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 0') --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) EXPERROR='rejected by ApprovalProgram' if [[ $RES != *"${EXPERROR}"* ]]; then date '+app-create-test FAIL txn with failing approval prog should be rejected %Y%m%d_%H%M%S' From dad6b8db0a19250f74f13d63efbf17143361dbe4 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 13 Jul 2020 20:43:56 -0400 Subject: [PATCH 100/267] Fix rare synchronization issue in accountUpdates (#1230) We have found a rare synchronization issue in accountUpdates. The issue is expressed when querying for a balance beyond the MaxBalLookback boundaries, where is can result in providing an incorrect result. --- ledger/accountdb_test.go | 6 +- ledger/acctupdates.go | 17 ++- ledger/acctupdates_test.go | 208 ++++++++++++++++++++++++++++++-- ledger/blockdb_test.go | 17 +-- ledger/catchpointwriter_test.go | 2 +- util/db/dbutil.go | 69 ++++++++++- 6 files changed, 291 insertions(+), 28 deletions(-) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index 44020b6cba..a7a51b5b78 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -166,7 +166,7 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. func TestAccountDBInit(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs := dbOpenTest(t) + dbs, _ := dbOpenTest(t, true) setDbLogging(t, dbs) defer dbs.close() @@ -187,7 +187,7 @@ func TestAccountDBInit(t *testing.T) { func TestAccountDBRound(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] - dbs := dbOpenTest(t) + dbs, _ := dbOpenTest(t, true) setDbLogging(t, dbs) defer dbs.close() @@ -214,7 +214,7 @@ func TestAccountDBRound(t *testing.T) { func BenchmarkReadingAllBalances(b *testing.B) { proto := config.Consensus[protocol.ConsensusCurrentVersion] //b.N = 50000 - dbs := dbOpenTest(b) + dbs, _ := dbOpenTest(b, true) setDbLogging(b, dbs) defer dbs.close() diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index be55302d96..055ebda9c9 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1008,7 +1008,8 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb var catchpointLabel string beforeUpdatingBalancesTime := time.Now() var trieBalancesHash crypto.Digest - err := au.dbs.wdb.Atomic(func(tx *sql.Tx) (err error) { + + err := au.dbs.wdb.AtomicCommitWriteLock(func(tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) if au.catchpointInterval > 0 { mc, err0 := makeMerkleCommitter(tx, false) @@ -1039,20 +1040,26 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) + if err != nil { + return err + } + if isCatchpointRound { trieBalancesHash, err = au.balancesTrie.RootHash() if err != nil { return } } - return err - }) + + return nil + }, &au.accountsMu) if err != nil { au.balancesTrie = nil au.log.Warnf("unable to advance account snapshot: %v", err) return } + if isCatchpointRound { catchpointLabel, err = au.accountsCreateCatchpointLabel(dbRound+basics.Round(offset)+lookback, roundTotals[offset], committedRoundDigest, trieBalancesHash) if err != nil { @@ -1066,7 +1073,6 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } } - au.accountsMu.Lock() if isCatchpointRound && catchpointLabel != "" { au.lastCatchpointLabel = catchpointLabel } @@ -1122,7 +1128,8 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb au.accountsMu.Unlock() if isCatchpointRound && au.archivalLedger && catchpointLabel != "" { - // start generating the catchpoint on a separate goroutine. + // generate the catchpoint file. This need to be done inline so that it will block any new accounts that from being written. + // the generateCatchpoint expects that the accounts data would not be modified in the background during it's execution. au.generateCatchpoint(basics.Round(offset)+dbRound+lookback, catchpointLabel, committedRoundDigest, updatingBalancesDuration) } diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 5a1f463109..51b2142e96 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -17,6 +17,7 @@ package ledger import ( + "bytes" "database/sql" "fmt" "os" @@ -36,22 +37,30 @@ import ( ) type mockLedgerForTracker struct { - dbs dbPair - blocks []blockEntry - log logging.Logger + dbs dbPair + blocks []blockEntry + log logging.Logger + filename string + inMemory bool } -func makeMockLedgerForTracker(t testing.TB) *mockLedgerForTracker { - dbs := dbOpenTest(t) +func makeMockLedgerForTracker(t testing.TB, inMemory bool) *mockLedgerForTracker { + dbs, fileName := dbOpenTest(t, inMemory) dblogger := logging.TestingLog(t) dblogger.SetLevel(logging.Info) dbs.rdb.SetLogger(dblogger) dbs.wdb.SetLogger(dblogger) - return &mockLedgerForTracker{dbs: dbs, log: dblogger} + return &mockLedgerForTracker{dbs: dbs, log: dblogger, filename: fileName, inMemory: inMemory} } func (ml *mockLedgerForTracker) close() { ml.dbs.close() + // delete the database files of non-memory instances. + if !ml.inMemory { + os.Remove(ml.filename) + os.Remove(ml.filename + "-shm") + os.Remove(ml.filename + "-wal") + } } func (ml *mockLedgerForTracker) Latest() basics.Round { @@ -223,7 +232,7 @@ func TestAcctUpdates(t *testing.T) { } proto := config.Consensus[protocol.ConsensusCurrentVersion] - ml := makeMockLedgerForTracker(t) + ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) @@ -304,7 +313,7 @@ func TestAcctUpdatesFastUpdates(t *testing.T) { } proto := config.Consensus[protocol.ConsensusCurrentVersion] - ml := makeMockLedgerForTracker(t) + ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) @@ -396,7 +405,7 @@ func BenchmarkBalancesChanges(b *testing.B) { proto := config.Consensus[protocolVersion] - ml := makeMockLedgerForTracker(b) + ml := makeMockLedgerForTracker(b, true) defer ml.close() initialRounds := uint64(1) ml.blocks = randomInitChain(protocolVersion, int(initialRounds)) @@ -529,7 +538,7 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { os.RemoveAll("./catchpoints") }() - ml := makeMockLedgerForTracker(t) + ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) accts := []map[basics.Address]basics.AccountData{randomAccounts(100000)} @@ -596,3 +605,182 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { } } } + +// The TestAcctUpdatesUpdatesCorrectness conduct a correctless test for the accounts update in the following way - +// Each account is initialized with 100 algos. +// On every round, each account move variable amount of funds to an accumulating account. +// The deltas for each accounts are picked by using the lookup method. +// At the end of the test, we verify that each account has the expected amount of algos. +// In addition, throughout the test, we check ( using lookup ) that the historical balances, *beyond* the +// lookback are generating either an error, or returning the correct amount. +func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { + if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { + t.Skip("This test is too slow on ARM and causes travis builds to time out") + } + + // create new protocol version, which has lower look back. + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestAcctUpdatesUpdatesCorrectness") + protoParams := config.Consensus[protocol.ConsensusCurrentVersion] + protoParams.MaxBalLookback = 5 + config.Consensus[testProtocolVersion] = protoParams + defer func() { + delete(config.Consensus, testProtocolVersion) + }() + + inMemory := true + + testFunction := func(t *testing.T) { + ml := makeMockLedgerForTracker(t, inMemory) + defer ml.close() + ml.blocks = randomInitChain(testProtocolVersion, 10) + + accts := []map[basics.Address]basics.AccountData{randomAccounts(9)} + + pooldata := basics.AccountData{} + pooldata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + pooldata.Status = basics.NotParticipating + accts[0][testPoolAddr] = pooldata + + sinkdata := basics.AccountData{} + sinkdata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 + sinkdata.Status = basics.NotParticipating + accts[0][testSinkAddr] = sinkdata + + var moneyAccounts []basics.Address + + for addr := range accts[0] { + if bytes.Compare(addr[:], testPoolAddr[:]) == 0 || bytes.Compare(addr[:], testSinkAddr[:]) == 0 { + continue + } + moneyAccounts = append(moneyAccounts, addr) + } + + moneyAccountsExpectedAmounts := make([][]uint64, 0) + // set all the accounts with 100 algos. + for _, addr := range moneyAccounts { + accountData := accts[0][addr] + accountData.MicroAlgos.Raw = 100 * 1000000 + accts[0][addr] = accountData + } + + au := &accountUpdates{} + au.initialize(config.GetDefaultLocal(), ".", protoParams, accts[0]) + defer au.close() + + err := au.loadFromDisk(ml) + require.NoError(t, err) + + // cover 10 genesis blocks + rewardLevel := uint64(0) + for i := 1; i < 10; i++ { + accts = append(accts, accts[0]) + + } + for i := 0; i < 10; i++ { + moneyAccountsExpectedAmounts = append(moneyAccountsExpectedAmounts, make([]uint64, len(moneyAccounts))) + for j := range moneyAccounts { + moneyAccountsExpectedAmounts[i][j] = 100 * 1000000 + } + } + + i := basics.Round(10) + roundCount := 50 + for ; i < basics.Round(10+roundCount); i++ { + updates := make(map[basics.Address]accountDelta) + moneyAccountsExpectedAmounts = append(moneyAccountsExpectedAmounts, make([]uint64, len(moneyAccounts))) + toAccount := moneyAccounts[0] + toAccountDataOld, err := au.lookup(i-1, toAccount, false) + require.NoError(t, err) + toAccountDataNew := toAccountDataOld + + for j := 1; j < len(moneyAccounts); j++ { + fromAccount := moneyAccounts[j] + + fromAccountDataOld, err := au.lookup(i-1, fromAccount, false) + require.NoError(t, err) + require.Equalf(t, moneyAccountsExpectedAmounts[i-1][j], fromAccountDataOld.MicroAlgos.Raw, "Account index : %d\nRound number : %d", j, i) + + fromAccountDataNew := fromAccountDataOld + + fromAccountDataNew.MicroAlgos.Raw -= uint64(i - 10) + toAccountDataNew.MicroAlgos.Raw += uint64(i - 10) + updates[fromAccount] = accountDelta{ + old: fromAccountDataOld, + new: fromAccountDataNew, + } + + moneyAccountsExpectedAmounts[i][j] = fromAccountDataNew.MicroAlgos.Raw + } + + moneyAccountsExpectedAmounts[i][0] = moneyAccountsExpectedAmounts[i-1][0] + uint64(len(moneyAccounts)-1)*uint64(i-10) + + // force to perform a test that goes directly to disk, and see if it has the expected values. + if uint64(i) > protoParams.MaxBalLookback+3 { + + // check the status at a historical time: + checkRound := uint64(i) - protoParams.MaxBalLookback - 2 + + testback := 1 + for j := 1; j < len(moneyAccounts); j++ { + if checkRound < uint64(testback) { + continue + } + acct, err := au.lookup(basics.Round(checkRound-uint64(testback)), moneyAccounts[j], false) + // we might get an error like "round 2 before dbRound 5", which is the success case, so we'll ignore it. + if err != nil { + // verify it's the expected error and not anything else. + var r1, r2 int + n, err2 := fmt.Sscanf(err.Error(), "round %d before dbRound %d", &r1, &r2) + require.NoErrorf(t, err2, "unable to parse : %v", err) + require.Equal(t, 2, n) + require.Less(t, r1, r2) + if testback > 1 { + testback-- + } + continue + } + // if we received no error, we want to make sure the reported amount is correct. + require.Equalf(t, moneyAccountsExpectedAmounts[checkRound-uint64(testback)][j], acct.MicroAlgos.Raw, "Account index : %d\nRound number : %d", j, checkRound) + testback++ + j-- + } + } + + updates[toAccount] = accountDelta{ + old: toAccountDataOld, + new: toAccountDataNew, + } + + blk := bookkeeping.Block{ + BlockHeader: bookkeeping.BlockHeader{ + Round: basics.Round(i), + }, + } + blk.RewardsLevel = rewardLevel + blk.CurrentProtocol = testProtocolVersion + + au.newBlock(blk, StateDelta{ + accts: updates, + hdr: &blk.BlockHeader, + }) + au.committedUpTo(i) + } + lastRound := i - 1 + au.waitAccountsWriting() + + for idx, addr := range moneyAccounts { + balance, err := au.lookup(lastRound, addr, false) + require.NoErrorf(t, err, "unable to retrieve balance for account idx %d %v", idx, addr) + if idx != 0 { + require.Equalf(t, 100*1000000-roundCount*(roundCount-1)/2, int(balance.MicroAlgos.Raw), "account idx %d %v has the wrong balance", idx, addr) + } else { + require.Equalf(t, 100*1000000+(len(moneyAccounts)-1)*roundCount*(roundCount-1)/2, int(balance.MicroAlgos.Raw), "account idx %d %v has the wrong balance", idx, addr) + } + + } + } + + t.Run("InMemoryDB", testFunction) + inMemory = false + t.Run("DiskDB", testFunction) +} diff --git a/ledger/blockdb_test.go b/ledger/blockdb_test.go index 96068b7116..a7cd259e77 100644 --- a/ledger/blockdb_test.go +++ b/ledger/blockdb_test.go @@ -19,6 +19,7 @@ package ledger import ( "database/sql" "fmt" + "strings" "testing" "github.com/stretchr/testify/require" @@ -31,11 +32,11 @@ import ( "github.com/algorand/go-algorand/protocol" ) -func dbOpenTest(t testing.TB) dbPair { - fn := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) - dbs, err := dbOpen(fn, true) - require.NoError(t, err) - return dbs +func dbOpenTest(t testing.TB, inMemory bool) (dbPair, string) { + fn := fmt.Sprintf("%s.%d", strings.ReplaceAll(t.Name(), "/", "."), crypto.RandUint64()) + dbs, err := dbOpen(fn, inMemory) + require.NoErrorf(t, err, "Filename : %s\nInMemory: %v", fn, inMemory) + return dbs, fn } func randomBlock(r basics.Round) blockEntry { @@ -116,7 +117,7 @@ func setDbLogging(t testing.TB, dbs dbPair) { } func TestBlockDBEmpty(t *testing.T) { - dbs := dbOpenTest(t) + dbs, _ := dbOpenTest(t, true) setDbLogging(t, dbs) defer dbs.close() @@ -130,7 +131,7 @@ func TestBlockDBEmpty(t *testing.T) { } func TestBlockDBInit(t *testing.T) { - dbs := dbOpenTest(t) + dbs, _ := dbOpenTest(t, true) setDbLogging(t, dbs) defer dbs.close() @@ -150,7 +151,7 @@ func TestBlockDBInit(t *testing.T) { } func TestBlockDBAppend(t *testing.T) { - dbs := dbOpenTest(t) + dbs, _ := dbOpenTest(t, true) setDbLogging(t, dbs) defer dbs.close() diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index ebb0202c12..cd1a85732c 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -135,7 +135,7 @@ func TestBasicCatchpointWriter(t *testing.T) { os.RemoveAll("./catchpoints") }() - ml := makeMockLedgerForTracker(t) + ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) accts := []map[basics.Address]basics.AccountData{randomAccounts(300)} diff --git a/util/db/dbutil.go b/util/db/dbutil.go index fdc1b75b85..42fd998690 100644 --- a/util/db/dbutil.go +++ b/util/db/dbutil.go @@ -56,6 +56,7 @@ var sqliteInitOnce sync.Once type Accessor struct { Handle *sql.DB readOnly bool + inMemory bool log logging.Logger } @@ -74,6 +75,7 @@ func MakeErasableAccessor(dbfilename string) (Accessor, error) { func makeAccessorImpl(dbfilename string, readOnly bool, inMemory bool, params []string) (Accessor, error) { var db Accessor db.readOnly = readOnly + db.inMemory = inMemory // SQLite3 driver we use (mattn/go-sqlite3) does not implement driver.DriverContext interface // that forces sql.Open calling sql.OpenDB and return a struct without any touches to the underlying driver. @@ -170,7 +172,7 @@ func Retry(fn func() error) (err error) { // getDecoratedLogger retruns a decorated logger that includes the readonly true/false, caller and extra fields. func (db *Accessor) getDecoratedLogger(fn idemFn, extras ...interface{}) logging.Logger { log := db.logger().With("readonly", db.readOnly) - _, file, line, ok := runtime.Caller(2) + _, file, line, ok := runtime.Caller(3) if ok { log = log.With("caller", fmt.Sprintf("%s:%d", file, line)) } @@ -185,9 +187,21 @@ func (db *Accessor) getDecoratedLogger(fn idemFn, extras ...interface{}) logging return log } +// IsSharedCacheConnection returns whether this connection was created using shared-cache connection or not. +// we use shared cache for in-memory databases +func (db *Accessor) IsSharedCacheConnection() bool { + return db.inMemory +} + // Atomic executes a piece of code with respect to the database atomically. // For transactions where readOnly is false, sync determines whether or not to wait for the result. func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { + return db.atomic(fn, nil, extras...) +} + +// Atomic executes a piece of code with respect to the database atomically. +// For transactions where readOnly is false, sync determines whether or not to wait for the result. +func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interface{}) (err error) { start := time.Now() // note that the sql library will drop panics inside an active transaction @@ -210,6 +224,29 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { var conn *sql.Conn ctx := context.Background() + commitWriteLockTaken := false + if commitLocker != nil && db.IsSharedCacheConnection() { + // When we're using in memory database, the sqlite implementation forces us to use a shared cache + // mode so that multiple connections ( i.e. read and write ) could share the database instance. + // ( it would also create issues between precompiled statements and regular atomic calls, as the former + // would generate a connection on the fly). + // when using a shared cache, we have to be aware that there are additional locking mechanisms that are + // internal to the sqlite. Two of them which play a role here are the sqlite_unlock_notify which + // prevents a shared cache locks from returning "database is busy" error and would block instead, and + // table level locks, which ensure that at any one time, a single table may have any number of active + // read-locks or a single active write lock. + // see https://www.sqlite.org/sharedcache.html for more details. + // These shared cache constrains are more strict than the WAL based concurrency limitations, which allows + // one writer and multiple readers at the same time. + // In particular, the shared cache limitation means that since a connection could become a writer, any syncronization + // operating that would prevent this operation from completing could result with a deadlock. + // This is the reason why for shared cache connections, we'll take the lock before starting the write transaction, + // and would keep it along. It will cause a degraded performance when using a shared cache connection + // compared to a private cache connection, but would grentee correct locking semantics. + commitLocker.Lock() + commitWriteLockTaken = true + } + for i := 0; (i == 0) || dbretry(err); i++ { if i > 0 { if i < infoTxRetries { @@ -225,11 +262,22 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { } if err != nil { + // fail case - unable to create database connection + if commitLocker != nil && commitWriteLockTaken { + commitLocker.Unlock() + } return } defer conn.Close() for i := 0; ; i++ { + // check if the lock was taken in previous iteration + if commitLocker != nil && (!db.IsSharedCacheConnection()) && commitWriteLockTaken { + // undo the lock. + commitLocker.Unlock() + commitWriteLockTaken = false + } + if i > 0 { if i < infoTxRetries { db.getDecoratedLogger(fn, extras).Infof("db.atomic: %d retries (last err: %v)", i, err) @@ -258,6 +306,12 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { } } + // if everytyhing went well, take the lock, as we're going to attempt to commit the transaction to database. + if commitLocker != nil && (!commitWriteLockTaken) && (!db.IsSharedCacheConnection()) { + commitLocker.Lock() + commitWriteLockTaken = true + } + err = tx.Commit() if err == nil { break @@ -266,6 +320,11 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { } } + // if we've errored, make sure to unlock the commitLocker ( if there is any ) + if err != nil && commitLocker != nil && commitWriteLockTaken { + commitLocker.Unlock() + } + end := time.Now() delta := end.Sub(start) if delta > time.Second { @@ -276,6 +335,14 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { return } +// AtomicCommitWriteLock executes a piece of code with respect to the database atomically. +// For transactions where readOnly is false, sync determines whether or not to wait for the result. +// The commitLocker is being taken before the transaction is committed. In case of an error, the lock would get released. +// on all success cases ( i.e. err = nil ) the lock would be taken. on all the fail cases, the lock would be released +func (db *Accessor) AtomicCommitWriteLock(fn idemFn, commitLocker sync.Locker, extras ...interface{}) (err error) { + return db.atomic(fn, commitLocker, extras...) +} + // URI returns the sqlite URI given a db filename as an input. func URI(filename string, readOnly bool, memory bool) string { uri := fmt.Sprintf("file:%s?_busy_timeout=%d&_synchronous=full", filename, busy) From e0b19a3bd66e7ab507116424af4cff6f3767d63f Mon Sep 17 00:00:00 2001 From: egieseke Date: Tue, 14 Jul 2020 07:43:14 -0400 Subject: [PATCH 101/267] Eric/update single go version (#1234) Updated specification of single go version to use what is specified in the get_golang_version.sh script. This is to avoid an issue with go version 1.14 where the patch version is no longer allowed to be specified in go.mod file. Added a check to make sure that the go version specified in the get_golang_version.sh script is compatible with the version specified go.mod, where the major and minor versions should match, and also patch version if specified in the go.mod file. --- go.mod | 2 +- scripts/configure_dev.sh | 2 +- scripts/deploy_linux_version.sh | 9 +++++++-- scripts/get_golang_version.sh | 16 +++++++++++++++- scripts/release/build/build_algod_docker.sh | 9 +++++++-- scripts/release/build/rpm/build.sh | 12 ++++++++---- scripts/release/common/setup.sh | 8 ++++++-- scripts/release/mule/package/docker/package.sh | 7 +++++++ scripts/travis/build.sh | 8 +++++++- scripts/travis/test.sh | 9 ++++++++- 10 files changed, 67 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 6f2a8c3622..85466178e1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/algorand/go-algorand -go 1.12.17 +go 1.12 require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d diff --git a/scripts/configure_dev.sh b/scripts/configure_dev.sh index 791095ea93..cfb8757717 100755 --- a/scripts/configure_dev.sh +++ b/scripts/configure_dev.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -set -ex +set -e HELP="Usage: $0 [-s] Installs host level dependencies necessary to build go-algorand. diff --git a/scripts/deploy_linux_version.sh b/scripts/deploy_linux_version.sh index aa023285f8..e32ce9bb92 100755 --- a/scripts/deploy_linux_version.sh +++ b/scripts/deploy_linux_version.sh @@ -77,6 +77,11 @@ echo export NETWORK=${NETWORK} >> ${TMPDIR}/deploy_linux_version_exec.sh echo scripts/deploy_private_version.sh -c \"${CHANNEL}\" -g \"${DEFAULTNETWORK}\" -n \"${NETWORK}\" -f \"${GENESISFILE}\" -b \"${S3_RELEASE_BUCKET}\" >> ${TMPDIR}/deploy_linux_version_exec.sh chmod +x ${TMPDIR}/deploy_linux_version_exec.sh -GOLANG_VERSION=$(./scripts/get_golang_version.sh) +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$(./scripts/get_golang_version.sh) +then + echo "${GOLANG_VERSION}" + exit 1 +fi sed "s|TMPDIR|${SUBDIR}|g" ${SRCPATH}/docker/build/Dockerfile-deploy > ${TMPDIR}/Dockerfile-deploy -docker build -f ${TMPDIR}/Dockerfile-deploy --build-arg GOLANG_VERSION=${GOLANG_VERSION} -t algorand-deploy . +docker build -f ${TMPDIR}/Dockerfile-deploy --build-arg GOLANG_VERSION="${GOLANG_VERSION}" -t algorand-deploy . diff --git a/scripts/get_golang_version.sh b/scripts/get_golang_version.sh index a5c2afc5e4..d984144825 100755 --- a/scripts/get_golang_version.sh +++ b/scripts/get_golang_version.sh @@ -1,2 +1,16 @@ #!/usr/bin/env bash -awk '/^go[ \t]+[0-9]+\.[0-9]+(\.[0-9]+)?[ \t]*$/{print $2}' go.mod +# Required GOLANG Version for project specified here +GOLANG_VERSION="1.12.17" + +# Check to make sure that it matches what is specified in go.mod + +GOMOD_VERSION=$(awk '/^go[ \t]+[0-9]+\.[0-9]+(\.[0-9]+)?[ \t]*$/{print $2}' go.mod) +TRIMMED_GOLANG_VERSION=$(echo ${GOLANG_VERSION} |awk -F. '{ print $1 "." $2 }') + +if ! [[ ${GOLANG_VERSION} == "${GOMOD_VERSION}" || ${TRIMMED_GOLANG_VERSION} == "${GOMOD_VERSION}" ]] +then + echo "go version mismatch, go mod version ${GOMOD_VERSION} does not match required version ${GOLANG_VERSION}" + exit 1; +else + echo ${GOLANG_VERSION} +fi diff --git a/scripts/release/build/build_algod_docker.sh b/scripts/release/build/build_algod_docker.sh index 309dffc036..464e6f565c 100755 --- a/scripts/release/build/build_algod_docker.sh +++ b/scripts/release/build/build_algod_docker.sh @@ -36,9 +36,14 @@ DOCKER_PKG_FILE="algod_docker_package_${CHANNEL_VERSION}.tar.gz" DOCKER_TAG="latest" DOCKER_IMAGE="algorand/algod_${CHANNEL_VERSION}:${DOCKER_TAG}" RESULT_DIR="${HOME}/node_pkg/" -DOCKERFILE="$HOME/go/src/github.com/algorand/go-algorand/docker/build/algod.Dockerfile" +DOCKERFILE="${HOME}/go/src/github.com/algorand/go-algorand/docker/build/algod.Dockerfile" START_ALGOD_FILE="start_algod_docker.sh" -GOLANG_VERSION=$(script/get_golang_version.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") +then + echo "${GOLANG_VERSION}" + exit 1 +fi echo "building '${DOCKERFILE}' with install file $ALGOD_INSTALL_TAR_FILE" cp "${ALGOD_INSTALL_TAR_FILE}" "./${INPUT_ALGOD_TAR_FILE}" diff --git a/scripts/release/build/rpm/build.sh b/scripts/release/build/rpm/build.sh index b421ed152b..35245d05bc 100755 --- a/scripts/release/build/rpm/build.sh +++ b/scripts/release/build/rpm/build.sh @@ -18,11 +18,15 @@ then echo There has been a problem cloning the "$BRANCH" branch. exit 1 fi - -# Install go version specified in go.mod and build its own copy of go-algorand. -GOLANG_VERSION=$(./scripts/get_golang_version.sh) +cd go-algorand +# Install go version specified by get_golang_version.sh and build its own copy of go-algorand. +if ! GOLANG_VERSION=$(./scripts/get_golang_version.sh) +then + echo "${GOLANG_VERSION}" + exit 1 +fi cd "${HOME}" -if ! curl -O https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz +if ! curl -O "https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz" then echo Golang could not be installed! exit 1 diff --git a/scripts/release/common/setup.sh b/scripts/release/common/setup.sh index e4a41e59af..0426a73d51 100755 --- a/scripts/release/common/setup.sh +++ b/scripts/release/common/setup.sh @@ -49,8 +49,12 @@ COMMIT_HASH=$(git rev-parse "${BRANCH}") export DEBIAN_FRONTEND=noninteractive -# Install go version specified in go.mod -GOLANG_VERSION=$(./scripts/get_golang_version.sh) +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$(./scripts/get_golang_version.sh) +then + echo "${GOLANG_VERSION}" + exit 1 +fi cd "${HOME}" if ! curl -O https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz then diff --git a/scripts/release/mule/package/docker/package.sh b/scripts/release/mule/package/docker/package.sh index b9e8a65878..92efb4310a 100755 --- a/scripts/release/mule/package/docker/package.sh +++ b/scripts/release/mule/package/docker/package.sh @@ -31,6 +31,13 @@ DOCKERFILE="./docker/build/algod.Dockerfile" START_ALGOD_FILE="./docker/release/start_algod_docker.sh" ALGOD_DOCKER_INIT="./docker/release/algod_docker_init.sh" +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$(./scripts/get_golang_version.sh) +then + echo "${GOLANG_VERSION}" + exit 1 +fi + echo "building '$DOCKERFILE' with install file $ALGOD_INSTALL_TAR_FILE" cp "$ALGOD_INSTALL_TAR_FILE" "/tmp/$INPUT_ALGOD_TAR_FILE" cp "$ALGOD_DOCKER_INIT" /tmp diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh index 5399269113..25bcb88705 100755 --- a/scripts/travis/build.sh +++ b/scripts/travis/build.sh @@ -34,10 +34,16 @@ SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" OS=$("${SCRIPTPATH}/../ostype.sh") ARCH=$("${SCRIPTPATH}/../archtype.sh") +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$("${SCRIPTPATH}/../get_golang_version.sh") +then + echo "${GOLANG_VERSION}" + exit 1 +fi curl -sL -o ~/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme chmod +x ~/gimme -eval $(~/gimme $("${SCRIPTPATH}/../get_golang_version.sh")) +eval $(~/gimme "${GOLANG_VERSION}") # travis sometimes fail to download a dependency. trying multiple times might help. for (( attempt=1; attempt<=5; attempt++ )) diff --git a/scripts/travis/test.sh b/scripts/travis/test.sh index ed1a898b38..179ae9e265 100755 --- a/scripts/travis/test.sh +++ b/scripts/travis/test.sh @@ -7,9 +7,16 @@ SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" OS=$("${SCRIPTPATH}/../ostype.sh") ARCH=$("${SCRIPTPATH}/../archtype.sh") +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$("${SCRIPTPATH}/../get_golang_version.sh") +then + echo "${GOLANG_VERSION}" + exit 1 +fi + curl -sL -o ~/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme chmod +x ~/gimme -eval $(~/gimme $("${SCRIPTPATH}/../get_golang_version.sh")) +eval $(~/gimme "${GOLANG_VERSION}") if [ "${OS}-${ARCH}" = "linux-arm" ]; then # for arm, no tests need to be invoked. From a1ed6d80b82ed2c8f655563e1d3cbf60cacf9e1d Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 14 Jul 2020 10:46:10 -0400 Subject: [PATCH 102/267] Add reloadLedger unit test --- ledger/acctupdates.go | 15 +++++++++++++-- ledger/ledger.go | 14 +++++++++----- ledger/ledger_test.go | 27 +++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index c848f7561b..a177f1c7d8 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -173,10 +173,10 @@ type accountUpdates struct { // otherwise, it would take it's time and perform periodic sleeps between chunks processing. catchpointSlowWriting chan struct{} - // ctx is the context for canceling any pending catchpoint generation operation + // ctx is the context for the committing go-routine. It's also used as the "parent" of the catchpoint generation operation. ctx context.Context - // ctxCancel is the canceling function canceling any pending catchpoint generation operation + // ctxCancel is the canceling function for canceling the commiting go-routine ( i.e. signaling the commiting go-routine that it's time to abort ) ctxCancel context.CancelFunc // deltasAccum stores the accumulated deltas for every round starting dbRound-1. @@ -190,6 +190,10 @@ type accountUpdates struct { // accountsWriting provides syncronization around the background writing of account balances. accountsWriting sync.WaitGroup + + // commitSyncerWaitGroup is the blocking channel for syncronizing closing the commitSyncer goroutine. Once it's closed, the + // commitSyncer can be assumed to have aborted. + commitSyncerClosed chan struct{} } type deferedCommit struct { @@ -209,6 +213,9 @@ func (au *accountUpdates) initialize(cfg config.Local, dbPathPrefix string, gene if cfg.CatchpointFileHistoryLength < -1 { au.catchpointFileHistoryLength = -1 } + // initialize the commitSyncerClosed with a closed channel ( since the commitSyncer go-routine is not active ) + au.commitSyncerClosed = make(chan struct{}) + close(au.commitSyncerClosed) } // loadFromDisk is the 2nd level initializtion, and is required before the accountUpdates becomes functional @@ -249,6 +256,8 @@ func (au *accountUpdates) close() { au.ctxCancel() } au.waitAccountsWriting() + // this would block until the commitSyncerClosed channel get closed. + <-au.commitSyncerClosed } // Lookup returns the accound data for a given address at a given round. The withRewards indicates whether the @@ -717,6 +726,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo close(au.catchpointWriting) au.ctx, au.ctxCancel = context.WithCancel(context.Background()) au.committedOffset = make(chan deferedCommit, 1) + au.commitSyncerClosed = make(chan struct{}) go au.commitSyncer(au.committedOffset) lastBalancesRound = au.dbRound @@ -1007,6 +1017,7 @@ func (au *accountUpdates) roundOffset(rnd basics.Round) (offset uint64, err erro } func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { + defer close(au.commitSyncerClosed) for { select { case committedOffset, ok := <-deferedCommits: diff --git a/ledger/ledger.go b/ledger/ledger.go index 4e63f072aa..e4a8eecd31 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -137,16 +137,20 @@ func OpenLedger( } func (l *Ledger) reloadLedger() error { - l.trackerMu.Lock() - defer l.trackerMu.Unlock() - - // close first. - l.trackers.close() + // similar to the Close function, we want to start by closing the blockQ first. The + // blockQ is having a sync goroutine which indirectly calls other trackers. We want to eliminate the path first, + // and follow up by taking the trackers lock. if l.blockQ != nil { l.blockQ.close() l.blockQ = nil } + l.trackerMu.Lock() + defer l.trackerMu.Unlock() + + // close the trackers. At this point, we already have the trackers write lock which ensures that noone is current using these. + l.trackers.close() + // reload. var err error l.blockQ, err = bqInit(l) diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index c3e37d1821..019958ae03 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -862,3 +862,30 @@ func testLedgerRegressionFaultyLeaseFirstValidCheck2f3880f7(t *testing.T, versio a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "should allow leasing payment transaction with newer FirstValid") } } + +func TestLedgerReload(t *testing.T) { + dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) + genesisInitState := getInitState() + const inMem = true + cfg := config.GetDefaultLocal() + cfg.Archival = true + log := logging.TestingLog(t) + l, err := OpenLedger(log, dbName, inMem, genesisInitState, cfg) + require.NoError(t, err) + defer l.Close() + + blk := genesisInitState.Block + + for i := 0; i < 128; i++ { + blk.BlockHeader.Round++ + blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) + l.AddBlock(blk, agreement.Certificate{}) + + if i%7 == 0 { + l.reloadLedger() + } + if i%13 == 0 { + l.WaitForCommit(blk.Round()) + } + } +} From cb81cfd78ca456ab4bb2f581a4641c444b86b226 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 15 Jul 2020 13:36:06 -0400 Subject: [PATCH 103/267] Add orphan entries to the check commands. (#1247) The existing algorelay know only how to update SRV and DNS entries. It does not remove them. This PR adds two-way checking to detect orphan entries that were left on the DNS system and do not corresponds to the provided relays json. --- cmd/algorelay/relayCmd.go | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/cmd/algorelay/relayCmd.go b/cmd/algorelay/relayCmd.go index bebc628b99..3bebad9c1c 100644 --- a/cmd/algorelay/relayCmd.go +++ b/cmd/algorelay/relayCmd.go @@ -182,7 +182,12 @@ var checkCmd = &cobra.Command{ results := make([]checkResult, 0) anyCheckError := false + relaysDNSAlias := make(map[string]bool) + enabledRelaysDNSAlias := make(map[string]bool) + relayHostNames := make(map[string]bool) + for _, relay := range relays { + relaysDNSAlias[relay.DNSAlias] = true if checkOne && relay.ID != recordIDArg { continue } @@ -190,6 +195,8 @@ var checkCmd = &cobra.Command{ if !relay.CheckSuccess { continue } + enabledRelaysDNSAlias[relay.DNSAlias] = true + relayHostNames[strings.Split(relay.Address, ":")[0]] = true const checkOnly = true name, port, err := ensureRelayStatus(checkOnly, relay, nameDomainArg, srvDomainArg, defaultPortArg, context) @@ -214,6 +221,48 @@ var checkCmd = &cobra.Command{ } } + // look for orphan _algobootstrap records that aren't represented by this relay file. + if context.bootstrap.entries != nil { + for bootstrap := range context.bootstrap.entries { + alias := strings.Split(bootstrap, ".")[0] + if enabledRelaysDNSAlias[alias] { + continue + } + if relaysDNSAlias[alias] { + fmt.Printf("WARN : disabled relay %s has a _algobootstrap entry\n", bootstrap) + } else { + fmt.Printf("INFO : orphan relay %s has a _algobootstrap entry\n", bootstrap) + } + } + } + + // look for orphan _metrics records that aren't represented by this relay file. + if context.metrics.entries != nil { + for metrics := range context.metrics.entries { + alias := strings.Split(metrics, ".")[0] + if enabledRelaysDNSAlias[alias] { + continue + } + if relaysDNSAlias[alias] { + fmt.Printf("WARN : disabled relay %s has a _metrics entry\n", metrics) + } else { + fmt.Printf("INFO : orphan relay %s has a _metrics entry\n", metrics) + } + } + } + for name, entry := range context.nameEntries { + if relayHostNames[name] { + continue + } + alias := strings.Split(entry, ".")[0] + if enabledRelaysDNSAlias[alias] { + // if we have an entry for that, than it just mean that it wasn't updated yet. + continue + } + fmt.Printf("INFO : orphan DNS entry %s -> %s\n", entry, name) + + } + if outputFileArg != "" { codecs.SaveObjectToFile(outputFileArg, &results, true) } From 88bc89a53c99e3ddcb7e6953d1358d1cfa3c266b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 15 Jul 2020 16:56:20 -0400 Subject: [PATCH 104/267] Implement database versioning support (#1243) This PR implements the core functionality for database versioning support. --- util/db/versioning.go | 58 +++++++++++++++++++++++ util/db/versioning_test.go | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 util/db/versioning.go create mode 100644 util/db/versioning_test.go diff --git a/util/db/versioning.go b/util/db/versioning.go new file mode 100644 index 0000000000..55db50fa90 --- /dev/null +++ b/util/db/versioning.go @@ -0,0 +1,58 @@ +// 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 . + +package db + +import ( + "context" + "database/sql" + "fmt" +) + +// this file contains database versioning that can be applied to any sqlite database. + +// GetUserVersion returns the user version field stored in the sqlite database +// if the database was never initiliazed with a version, it would return 0 as the version. +func GetUserVersion(ctx context.Context, tx *sql.Tx) (userVersion int32, err error) { + + err = tx.QueryRowContext(ctx, "PRAGMA user_version").Scan(&userVersion) + // it's not really supposed to happen with a user_version, since the above would always succeed, but + // we want to have it so that we can align with the SQL statements "correct handling practices". + if err == sql.ErrNoRows { + err = nil + userVersion = 0 + } + return +} + +// SetUserVersion sets the userVersion as the new user version, and return the old version. +func SetUserVersion(ctx context.Context, tx *sql.Tx, userVersion int32) (previousUserVersion int32, err error) { + previousUserVersion, err = GetUserVersion(ctx, tx) + if err != nil { + return + } + if previousUserVersion == userVersion { + return + } + _, err = tx.ExecContext(ctx, fmt.Sprintf("PRAGMA user_version = %d", userVersion)) + if err != nil { + // if we're aborting due to an error, clear the previousUserVersion so that + // on all error cases we'll be returning zero. + previousUserVersion = 0 + return + } + return +} diff --git a/util/db/versioning_test.go b/util/db/versioning_test.go new file mode 100644 index 0000000000..668f0bec69 --- /dev/null +++ b/util/db/versioning_test.go @@ -0,0 +1,94 @@ +// 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 . + +package db + +import ( + "context" + "database/sql" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func testVersioning(t *testing.T, inMemory bool) { + acc, err := MakeAccessor("fn.db", false, inMemory) + require.NoError(t, err) + if !inMemory { + defer os.Remove("fn.db") + defer os.Remove("fn.db-shm") + defer os.Remove("fn.db-wal") + } + + conn, err := acc.Handle.Conn(context.Background()) + require.NoError(t, err) + + tx, err := conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: false}) + require.NoError(t, err) + + ver, err := GetUserVersion(context.Background(), tx) + require.NoError(t, err) + require.Equal(t, int32(0), ver) + + previousVersion, err := SetUserVersion(context.Background(), tx, 5) + require.NoError(t, err) + require.Equal(t, int32(0), previousVersion) + + previousVersion, err = SetUserVersion(context.Background(), tx, 9) + require.NoError(t, err) + require.Equal(t, int32(5), previousVersion) + + // check that expired context doesn't work: + expiredContext, expiredContextCancelFunc := context.WithCancel(context.Background()) + expiredContextCancelFunc() + ver, err = GetUserVersion(expiredContext, tx) + require.Equal(t, expiredContext.Err(), err) + require.Equal(t, int32(0), ver) + + ver, err = SetUserVersion(expiredContext, tx, 15) + require.Equal(t, expiredContext.Err(), err) + require.Equal(t, int32(0), ver) + + tx.Commit() + + tx, err = conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: false}) + require.NoError(t, err) + + previousVersion, err = SetUserVersion(context.Background(), tx, 2) + require.NoError(t, err) + require.Equal(t, int32(9), previousVersion) + + tx.Rollback() + + tx, err = conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: false}) + require.NoError(t, err) + + ver, err = GetUserVersion(context.Background(), tx) + require.NoError(t, err) + require.Equal(t, int32(9), ver) + + tx.Commit() + + conn.Close() + acc.Close() + +} + +func TestVersioning(t *testing.T) { + t.Run("InMem", func(t *testing.T) { testVersioning(t, true) }) + t.Run("OnDisk", func(t *testing.T) { testVersioning(t, false) }) +} From efc00c29add7c2e9bacedc5748bdfe6c11646d21 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 15 Jul 2020 20:40:33 -0400 Subject: [PATCH 105/267] Fix StateDelta conversion in Dryrun (#1249) StateDelta conversion in Dryrun incorrectly used pointers to in-loop variable. --- daemon/algod/api/server/v2/dryrun.go | 23 +++++++----- daemon/algod/api/server/v2/dryrun_test.go | 44 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index e3ec740ddf..3d5ef4129f 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -494,15 +494,20 @@ func StateDeltaToStateDelta(sd basics.StateDelta) *generated.StateDelta { gsd := make(generated.StateDelta, 0, len(sd)) for k, v := range sd { - bytes := base64.StdEncoding.EncodeToString([]byte(v.Bytes)) - gsd = append(gsd, generated.EvalDeltaKeyValue{ - Key: k, - Value: generated.EvalDelta{ - Action: uint64(v.Action), - Uint: &v.Uint, - Bytes: &bytes, - }, - }) + value := generated.EvalDelta{Action: uint64(v.Action)} + if v.Action == basics.SetBytesAction { + bytesVal := base64.StdEncoding.EncodeToString([]byte(v.Bytes)) + value.Bytes = &bytesVal + } else if v.Action == basics.SetUintAction { + uintVal := v.Uint + value.Uint = &uintVal + } + // basics.DeleteAction does not require Uint/Bytes + kv := generated.EvalDeltaKeyValue{ + Key: k, + Value: value, + } + gsd = append(gsd, kv) } return &gsd diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index c910d3448b..7e974a689e 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -928,3 +928,47 @@ func TestDryrunRequestJSON(t *testing.T) { logResponse(t, &response) } } + +func TestStateDeltaToStateDelta(t *testing.T) { + t.Parallel() + sd := basics.StateDelta{ + "intkey": { + Action: basics.SetUintAction, + Uint: 11, + }, + "byteskey": { + Action: basics.SetBytesAction, + Bytes: "test", + }, + "delkey": { + Action: basics.DeleteAction, + }, + } + gsd := StateDeltaToStateDelta(sd) + require.Equal(t, 3, len(*gsd)) + + var keys []string + // test with a loop because sd is a map and iteration order is random + for _, item := range *gsd { + if item.Key == "intkey" { + require.Equal(t, uint64(1), item.Value.Action) + require.NotNil(t, item.Value.Uint) + require.Equal(t, uint64(11), *item.Value.Uint) + require.Nil(t, item.Value.Bytes) + } else if item.Key == "byteskey" { + require.Equal(t, uint64(2), item.Value.Action) + require.Nil(t, item.Value.Uint) + require.NotNil(t, item.Value.Bytes) + require.Equal(t, base64.StdEncoding.EncodeToString([]byte("test")), *item.Value.Bytes) + } else if item.Key == "delkey" { + require.Equal(t, uint64(3), item.Value.Action) + require.Nil(t, item.Value.Uint) + require.Nil(t, item.Value.Bytes) + } + keys = append(keys, item.Key) + } + require.Equal(t, 3, len(keys)) + require.Contains(t, keys, "intkey") + require.Contains(t, keys, "byteskey") + require.Contains(t, keys, "delkey") +} From 939b9e8ae6e9b0f79a7be4e6920dc62b2cef9265 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 16 Jul 2020 15:13:19 -0400 Subject: [PATCH 106/267] Avoid warning messages on expected long transactions (#1246) This PR extends the database accessor functionality by allowing long running transactions to extend their "legit" execution runtime. This would allow the database logging logic to suppress undesired warnings caused by long running transactions. --- agreement/persistence.go | 8 +-- agreement/persistence_test.go | 7 +-- cmd/catchpointdump/file.go | 2 +- data/account/account.go | 7 +-- data/account/participation.go | 9 ++-- data/account/participation_test.go | 3 +- ledger/acctupdates.go | 6 +-- ledger/acctupdates_test.go | 3 +- ledger/archival_test.go | 9 ++-- ledger/blockqueue.go | 15 +++--- ledger/catchpointwriter.go | 4 +- ledger/catchupaccessor.go | 20 +++---- ledger/ledger.go | 4 +- libgoal/participation.go | 5 +- node/indexer/db.go | 3 +- util/db/dbutil.go | 48 +++++++++++++---- util/db/dbutil_test.go | 84 ++++++++++++++++++++++++------ util/db/perf_test.go | 18 +++---- 18 files changed, 170 insertions(+), 85 deletions(-) diff --git a/agreement/persistence.go b/agreement/persistence.go index c6cc4c99f6..974b816540 100644 --- a/agreement/persistence.go +++ b/agreement/persistence.go @@ -73,7 +73,7 @@ func persist(log serviceLogger, crash db.Accessor, Round basics.Round, Period pe log.with(logEvent).Info("persisted state to the database") }() - err = crash.Atomic(func(tx *sql.Tx) error { + err = crash.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("insert or replace into Service (rowid, data) values (1, ?)", raw) return err }) @@ -91,7 +91,7 @@ func persist(log serviceLogger, crash db.Accessor, Round basics.Round, Period pe func reset(log logging.Logger, crash db.Accessor) (err error) { logging.Base().Infof("reset (agreement): resetting crash state") - err = crash.Atomic(func(tx *sql.Tx) (res error) { + err = crash.Atomic(func(ctx context.Context, tx *sql.Tx) (res error) { // we could not retrieve our state, so wipe it _, err := tx.Exec("delete from Service") if err != nil { @@ -114,11 +114,11 @@ func restore(log logging.Logger, crash db.Accessor) (raw []byte, err error) { } }() - crash.Atomic(func(tx *sql.Tx) error { + crash.Atomic(func(ctx context.Context, tx *sql.Tx) error { return agreeInstallDatabase(tx) }) // ignore error - err = crash.Atomic(func(tx *sql.Tx) (res error) { + err = crash.Atomic(func(ctx context.Context, tx *sql.Tx) (res error) { var reset bool defer func() { if !reset { diff --git a/agreement/persistence_test.go b/agreement/persistence_test.go index ecbeac21da..9b6f8520ed 100644 --- a/agreement/persistence_test.go +++ b/agreement/persistence_test.go @@ -17,6 +17,7 @@ package agreement import ( + "context" "database/sql" "testing" "time" @@ -85,7 +86,7 @@ func TestAgreementPersistence(t *testing.T) { require.NoError(t, err) defer accessor.Close() - accessor.Atomic(func(tx *sql.Tx) error { + accessor.Atomic(func(ctx context.Context, tx *sql.Tx) error { return agreeInstallDatabase(tx) }) // ignore error @@ -112,7 +113,7 @@ func BenchmarkAgreementPersistence(b *testing.B) { accessor, _ := db.MakeAccessor(b.Name()+"_crash.db", false, true) defer accessor.Close() - accessor.Atomic(func(tx *sql.Tx) error { + accessor.Atomic(func(ctx context.Context, tx *sql.Tx) error { return agreeInstallDatabase(tx) }) // ignore error @@ -138,7 +139,7 @@ func BenchmarkAgreementPersistenceRecovery(b *testing.B) { accessor, _ := db.MakeAccessor(b.Name()+"_crash.db", false, true) defer accessor.Close() - accessor.Atomic(func(tx *sql.Tx) error { + accessor.Atomic(func(ctx context.Context, tx *sql.Tx) error { return agreeInstallDatabase(tx) }) // ignore error diff --git a/cmd/catchpointdump/file.go b/cmd/catchpointdump/file.go index bba3a444fc..fbb080b391 100644 --- a/cmd/catchpointdump/file.go +++ b/cmd/catchpointdump/file.go @@ -162,7 +162,7 @@ func printAccountsDatabase(databaseName string, fileHeader ledger.CatchpointFile totals.NotParticipating.Money.Raw, totals.NotParticipating.RewardUnits, totals.RewardsLevel) } - return dbAccessor.Atomic(func(tx *sql.Tx) (err error) { + return dbAccessor.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { if fileHeader.Version == 0 { var totals ledger.AccountTotals id := "" diff --git a/data/account/account.go b/data/account/account.go index 7e3f877448..ae4f4a3565 100644 --- a/data/account/account.go +++ b/data/account/account.go @@ -18,6 +18,7 @@ package account //go:generate dbgen -i root.sql -p account -n root -o rootInstall.go -h ../../scripts/LICENSE_HEADER import ( + "context" "database/sql" "fmt" @@ -54,7 +55,7 @@ func ImportRoot(store db.Accessor, seed [32]byte) (acc Root, err error) { s := crypto.GenerateSignatureSecrets(seed) raw := protocol.Encode(s) - err = store.Atomic(func(tx *sql.Tx) error { + err = store.Atomic(func(ctx context.Context, tx *sql.Tx) error { err := rootInstallDatabase(tx) if err != nil { return fmt.Errorf("ImportRoot: failed to install database: %v", err) @@ -86,7 +87,7 @@ func ImportRoot(store db.Accessor, seed [32]byte) (acc Root, err error) { func RestoreRoot(store db.Accessor) (acc Root, err error) { var raw []byte - err = store.Atomic(func(tx *sql.Tx) error { + err = store.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int row := tx.QueryRow("select count(*) from RootAccount") err := row.Scan(&nrows) @@ -141,7 +142,7 @@ func RestoreParticipation(store db.Accessor) (acc Participation, err error) { return } - err = store.Atomic(func(tx *sql.Tx) error { + err = store.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int row := tx.QueryRow("select count(*) from ParticipationAccount") err := row.Scan(&nrows) diff --git a/data/account/participation.go b/data/account/participation.go index c2f0c1d837..ccb619137b 100644 --- a/data/account/participation.go +++ b/data/account/participation.go @@ -17,6 +17,7 @@ package account import ( + "context" "database/sql" "fmt" "github.com/algorand/go-algorand/logging" @@ -91,7 +92,7 @@ func (part Participation) DeleteOldKeys(current basics.Round, proto config.Conse errorCh := make(chan error, 1) deleteOldKeys := func(encodedVotingSecrets []byte) { - errorCh <- part.Store.Atomic(func(tx *sql.Tx) error { + errorCh <- part.Store.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("UPDATE ParticipationAccount SET voting=?", encodedVotingSecrets) if err != nil { return fmt.Errorf("Participation.DeleteOldKeys: failed to update account: %v", err) @@ -108,7 +109,7 @@ func (part Participation) DeleteOldKeys(current basics.Round, proto config.Conse // PersistNewParent writes a new parent address to the partkey database. func (part Participation) PersistNewParent() error { - return part.Store.Atomic(func(tx *sql.Tx) error { + return part.Store.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("UPDATE ParticipationAccount SET parent=?", part.Parent[:]) return err }) @@ -195,7 +196,7 @@ func (part Participation) Persist() error { voting := part.Voting.Snapshot() rawVoting := protocol.Encode(&voting) - return part.Store.Atomic(func(tx *sql.Tx) error { + return part.Store.Atomic(func(ctx context.Context, tx *sql.Tx) error { err := partInstallDatabase(tx) if err != nil { return fmt.Errorf("Participation.persist: failed to install database: %v", err) @@ -213,7 +214,7 @@ func (part Participation) Persist() error { // Migrate is called when loading participation keys. // Calls through to the migration helper and returns the result. func Migrate(partDB db.Accessor) error { - return partDB.Atomic(func(tx *sql.Tx) error { + return partDB.Atomic(func(ctx context.Context, tx *sql.Tx) error { return partMigrate(tx) }) } diff --git a/data/account/participation_test.go b/data/account/participation_test.go index 5eb0767fec..c24fda04d3 100644 --- a/data/account/participation_test.go +++ b/data/account/participation_test.go @@ -17,6 +17,7 @@ package account import ( + "context" "database/sql" "testing" @@ -52,7 +53,7 @@ func TestParticipation_NewDB(t *testing.T) { } func getSchemaVersions(db db.Accessor) (versions map[string]int, err error) { - err = db.Atomic(func(tx *sql.Tx) (err error) { + err = db.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { rows, err := tx.Query("SELECT tablename, version FROM schema") if err != nil { return diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 055ebda9c9..c4df4328ae 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -598,7 +598,7 @@ func (au *accountUpdates) totals(rnd basics.Round) (totals AccountTotals, err er func (au *accountUpdates) getCatchpointStream(round basics.Round) (io.ReadCloser, error) { dbFileName := "" - err := au.dbs.rdb.Atomic(func(tx *sql.Tx) (err error) { + err := au.dbs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { dbFileName, _, _, err = getCatchpoint(tx, round) return }) @@ -695,7 +695,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo } lastestBlockRound = l.Latest() - err = au.dbs.wdb.Atomic(func(tx *sql.Tx) error { + err = au.dbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error au.dbRound, err0 = au.accountsInitialize(tx) if err0 != nil { @@ -1009,7 +1009,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb beforeUpdatingBalancesTime := time.Now() var trieBalancesHash crypto.Digest - err := au.dbs.wdb.AtomicCommitWriteLock(func(tx *sql.Tx) (err error) { + err := au.dbs.wdb.AtomicCommitWriteLock(func(ctx context.Context, tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) if au.catchpointInterval > 0 { mc, err0 := makeMerkleCommitter(tx, false) diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 51b2142e96..bec76282bb 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -18,6 +18,7 @@ package ledger import ( "bytes" + "context" "database/sql" "fmt" "os" @@ -115,7 +116,7 @@ func (au *accountUpdates) allBalances(rnd basics.Round) (bals map[basics.Address return } - err = au.dbs.rdb.Atomic(func(tx *sql.Tx) error { + err = au.dbs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error bals, err0 = accountsAll(tx) return err0 diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 0890ed0d78..2019d4a9ff 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -17,6 +17,7 @@ package ledger import ( + "context" "crypto/rand" "database/sql" "fmt" @@ -180,7 +181,7 @@ func TestArchivalRestart(t *testing.T) { l.WaitForCommit(blk.Round()) var latest, earliest basics.Round - err = l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { latest, err = blockLatest(tx) require.NoError(t, err) @@ -197,7 +198,7 @@ func TestArchivalRestart(t *testing.T) { require.NoError(t, err) defer l.Close() - err = l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { latest, err = blockLatest(tx) require.NoError(t, err) @@ -670,7 +671,7 @@ func TestArchivalFromNonArchival(t *testing.T) { l.WaitForCommit(blk.Round()) var latest, earliest basics.Round - err = l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { latest, err = blockLatest(tx) require.NoError(t, err) @@ -690,7 +691,7 @@ func TestArchivalFromNonArchival(t *testing.T) { require.NoError(t, err) defer l.Close() - err = l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { latest, err = blockLatest(tx) require.NoError(t, err) diff --git a/ledger/blockqueue.go b/ledger/blockqueue.go index 6f7dfb3cd4..6b889ae7c9 100644 --- a/ledger/blockqueue.go +++ b/ledger/blockqueue.go @@ -17,6 +17,7 @@ package ledger import ( + "context" "database/sql" "fmt" "sync" @@ -53,7 +54,7 @@ func bqInit(l *Ledger) (*blockQueue, error) { bq.l = l bq.running = true bq.closed = make(chan struct{}) - err := bq.l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err := bq.l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error bq.lastCommitted, err0 = blockLatest(tx) return err0 @@ -100,7 +101,7 @@ func (bq *blockQueue) syncer() { workQ := bq.q bq.mu.Unlock() - err := bq.l.blockDBs.wdb.Atomic(func(tx *sql.Tx) error { + err := bq.l.blockDBs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { for _, e := range workQ { err0 := blockPut(tx, e.block, e.cert) if err0 != nil { @@ -133,7 +134,7 @@ func (bq *blockQueue) syncer() { bq.mu.Unlock() minToSave := bq.l.notifyCommit(committed) - err = bq.l.blockDBs.wdb.Atomic(func(tx *sql.Tx) error { + err = bq.l.blockDBs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { return blockForgetBefore(tx, minToSave) }) if err != nil { @@ -244,7 +245,7 @@ func (bq *blockQueue) getBlock(r basics.Round) (blk bookkeeping.Block, err error return } - err = bq.l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = bq.l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error blk, err0 = blockGet(tx, r) return err0 @@ -263,7 +264,7 @@ func (bq *blockQueue) getBlockHdr(r basics.Round) (hdr bookkeeping.BlockHeader, return } - err = bq.l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = bq.l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error hdr, err0 = blockGetHdr(tx, r) return err0 @@ -286,7 +287,7 @@ func (bq *blockQueue) getEncodedBlockCert(r basics.Round) (blk []byte, cert []by return } - err = bq.l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = bq.l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error blk, cert, err0 = blockGetEncodedCert(tx, r) return err0 @@ -305,7 +306,7 @@ func (bq *blockQueue) getBlockCert(r basics.Round) (blk bookkeeping.Block, cert return } - err = bq.l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = bq.l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error blk, cert, err0 = blockGetCert(tx, r) return err0 diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index 392639eb8c..0d059f655d 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -221,7 +221,7 @@ func (cw *catchpointWriter) WriteStep(ctx context.Context) (more bool, err error } } -func (cw *catchpointWriter) readDatabaseStep(tx *sql.Tx) (err error) { +func (cw *catchpointWriter) readDatabaseStep(ctx context.Context, tx *sql.Tx) (err error) { cw.balancesChunk.Balances, err = encodedAccountsRange(tx, cw.balancesOffset, BalancesPerCatchpointFileChunk) if err == nil { cw.balancesOffset += BalancesPerCatchpointFileChunk @@ -229,7 +229,7 @@ func (cw *catchpointWriter) readDatabaseStep(tx *sql.Tx) (err error) { return } -func (cw *catchpointWriter) readHeaderFromDatabase(tx *sql.Tx) (err error) { +func (cw *catchpointWriter) readHeaderFromDatabase(ctx context.Context, tx *sql.Tx) (err error) { var header CatchpointFileHeader header.BalancesRound, _, err = accountsRound(tx) if err != nil { diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index 288adbb823..53c76d54b9 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -173,7 +173,7 @@ func (c *CatchpointCatchupAccessorImpl) SetLabel(ctx context.Context, label stri // ResetStagingBalances resets the current staging balances, preparing for a new set of balances to be added func (c *CatchpointCatchupAccessorImpl) ResetStagingBalances(ctx context.Context, newCatchup bool) (err error) { wdb := c.ledger.trackerDB().wdb - err = wdb.Atomic(func(tx *sql.Tx) (err error) { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { err = resetCatchpointStagingBalances(ctx, tx, newCatchup) if err != nil { return fmt.Errorf("unable to reset catchpoint catchup balances : %v", err) @@ -247,7 +247,7 @@ func (c *CatchpointCatchupAccessorImpl) processStagingContent(ctx context.Contex // later on: // TotalAccounts, TotalAccounts, Catchpoint, BlockHeaderDigest, BalancesRound wdb := c.ledger.trackerDB().wdb - err = wdb.Atomic(func(tx *sql.Tx) (err error) { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { sq, err := accountsDbInit(tx, tx) if err != nil { return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to initialize accountsDbInit: %v", err) @@ -284,7 +284,7 @@ func (c *CatchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte } wdb := c.ledger.trackerDB().wdb - err = wdb.Atomic(func(tx *sql.Tx) (err error) { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { // create the merkle trie for the balances mc, err0 := makeMerkleCommitter(tx, true) if err0 != nil { @@ -378,7 +378,7 @@ func (c *CatchpointCatchupAccessorImpl) VerifyCatchpoint(ctx context.Context, bl } blockRound = basics.Round(iRound) - err = rdb.Atomic(func(tx *sql.Tx) (err error) { + err = rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { // create the merkle trie for the balances mc, err0 := makeMerkleCommitter(tx, true) if err0 != nil { @@ -423,7 +423,7 @@ func (c *CatchpointCatchupAccessorImpl) StoreBalancesRound(ctx context.Context, // trust the one in the catchpoint file header, so we'll calculate it ourselves. balancesRound := blk.Round() - basics.Round(config.Consensus[blk.CurrentProtocol].MaxBalLookback) wdb := c.ledger.trackerDB().wdb - err = wdb.Atomic(func(tx *sql.Tx) (err error) { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { sq, err := accountsDbInit(tx, tx) _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBalancesRound, uint64(balancesRound)) if err != nil { @@ -437,7 +437,7 @@ func (c *CatchpointCatchupAccessorImpl) StoreBalancesRound(ctx context.Context, // StoreFirstBlock stores a single block to the blocks database. func (c *CatchpointCatchupAccessorImpl) StoreFirstBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { blockDbs := c.ledger.blockDB() - err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { + err = blockDbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { return blockStartCatchupStaging(tx, *blk) }) if err != nil { @@ -449,7 +449,7 @@ func (c *CatchpointCatchupAccessorImpl) StoreFirstBlock(ctx context.Context, blk // StoreBlock stores a single block to the blocks database. func (c *CatchpointCatchupAccessorImpl) StoreBlock(ctx context.Context, blk *bookkeeping.Block) (err error) { blockDbs := c.ledger.blockDB() - err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { + err = blockDbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { return blockPutStaging(tx, *blk) }) if err != nil { @@ -461,7 +461,7 @@ func (c *CatchpointCatchupAccessorImpl) StoreBlock(ctx context.Context, blk *boo // FinishBlocks concludes the catchup of the blocks database. func (c *CatchpointCatchupAccessorImpl) FinishBlocks(ctx context.Context, applyChanges bool) (err error) { blockDbs := c.ledger.blockDB() - err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { + err = blockDbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { if applyChanges { return blockCompleteCatchup(tx) } @@ -476,7 +476,7 @@ func (c *CatchpointCatchupAccessorImpl) FinishBlocks(ctx context.Context, applyC // EnsureFirstBlock ensure that we have a single block in the staging block table, and returns that block func (c *CatchpointCatchupAccessorImpl) EnsureFirstBlock(ctx context.Context) (blk bookkeeping.Block, err error) { blockDbs := c.ledger.blockDB() - err = blockDbs.wdb.Atomic(func(tx *sql.Tx) (err error) { + err = blockDbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { blk, err = blockEnsureSingleBlock(tx) return }) @@ -504,7 +504,7 @@ func (c *CatchpointCatchupAccessorImpl) CompleteCatchup(ctx context.Context) (er // finishBalances concludes the catchup of the balances(tracker) database. func (c *CatchpointCatchupAccessorImpl) finishBalances(ctx context.Context) (err error) { wdb := c.ledger.trackerDB().wdb - err = wdb.Atomic(func(tx *sql.Tx) (err error) { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { var balancesRound uint64 var totals AccountTotals diff --git a/ledger/ledger.go b/ledger/ledger.go index a532c21028..9a3908c804 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -114,7 +114,7 @@ func OpenLedger( l.blockDBs.rdb.SetLogger(log) l.blockDBs.wdb.SetLogger(log) - err = l.blockDBs.wdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { return initBlocksDB(tx, l, []bookkeeping.Block{genesisInitState.Block}, cfg.Archival) }) if err != nil { @@ -176,7 +176,7 @@ func (l *Ledger) reloadLedger() error { // verifyMatchingGenesisHash tests to see that the latest block header pointing to the same genesis hash provided in genesisHash. func (l *Ledger) verifyMatchingGenesisHash() (err error) { // Check that the genesis hash, if present, matches. - err = l.blockDBs.rdb.Atomic(func(tx *sql.Tx) error { + err = l.blockDBs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { latest, err := blockLatest(tx) if err != nil { return err diff --git a/libgoal/participation.go b/libgoal/participation.go index 6acd607ea6..7b9fe82bdf 100644 --- a/libgoal/participation.go +++ b/libgoal/participation.go @@ -82,18 +82,17 @@ func (c *Client) chooseParticipation(address basics.Address, round basics.Round) return } - nilParticipation := account.Participation{} // Loop through each of the files; pick the one that expires farthest in the future. var expiry basics.Round for _, info := range files { // Use above lambda so the deferred handle closure happens each loop partCandidate, err := checkIfFileIsDesiredKey(info, expiry) - if err == nil && partCandidate != nilParticipation { + if err == nil && (!partCandidate.Parent.IsZero()) { part = partCandidate expiry = part.LastValid } } - if part == nilParticipation { + if part.Parent.IsZero() { // Couldn't find one err = fmt.Errorf("Couldn't find a participation key database for address %v valid at round %v in directory %v", address.GetUserAddress(), round, keyDir) return diff --git a/node/indexer/db.go b/node/indexer/db.go index 5cea65cbb8..99469e48c6 100644 --- a/node/indexer/db.go +++ b/node/indexer/db.go @@ -17,6 +17,7 @@ package indexer import ( + "context" "database/sql" "fmt" @@ -100,7 +101,7 @@ func MakeIndexerDB(dbPath string, inMemory bool) (*DB, error) { // AddBlock takes an Algorand block and stores its transactions in the DB. func (idb *DB) AddBlock(b bookkeeping.Block) error { - err := idb.dbw.Atomic(func(tx *sql.Tx) error { + err := idb.dbw.Atomic(func(ctx context.Context, tx *sql.Tx) error { // Get last block rnd, err := idb.MaxRound() diff --git a/util/db/dbutil.go b/util/db/dbutil.go index 42fd998690..f6aa00cdc6 100644 --- a/util/db/dbutil.go +++ b/util/db/dbutil.go @@ -60,6 +60,13 @@ type Accessor struct { log logging.Logger } +// txExecutionContext contains the data that is associated with every created transaction +// before sending it to the user-defined callback. This allows the callback function to +// make changes to the execution setting of an ongoing transaction. +type txExecutionContext struct { + deadline time.Time +} + // MakeAccessor creates a new Accessor. func MakeAccessor(dbfilename string, readOnly bool, inMemory bool) (Accessor, error) { return makeAccessorImpl(dbfilename, readOnly, inMemory, []string{"_journal_mode=wal"}) @@ -202,10 +209,10 @@ func (db *Accessor) Atomic(fn idemFn, extras ...interface{}) (err error) { // Atomic executes a piece of code with respect to the database atomically. // For transactions where readOnly is false, sync determines whether or not to wait for the result. func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interface{}) (err error) { - start := time.Now() + atomicDeadline := time.Now().Add(time.Second) // note that the sql library will drop panics inside an active transaction - guardedFn := func(tx *sql.Tx) (err error) { + guardedFn := func(ctx context.Context, tx *sql.Tx) (err error) { defer func() { if r := recover(); r != nil { var ok bool @@ -216,7 +223,7 @@ func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interf } }() - err = fn(tx) + err = fn(ctx, tx) return } @@ -296,7 +303,12 @@ func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interf break } - err = guardedFn(tx) + // create a transaction context data + txContextData := &txExecutionContext{ + deadline: atomicDeadline, + } + + err = guardedFn(context.WithValue(ctx, tx, txContextData), tx) if err != nil { tx.Rollback() if dbretry(err) { @@ -314,6 +326,8 @@ func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interf err = tx.Commit() if err == nil { + // update the deadline, as it might have been updated. + atomicDeadline = txContextData.deadline break } else if !dbretry(err) { break @@ -325,13 +339,25 @@ func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interf commitLocker.Unlock() } - end := time.Now() - delta := end.Sub(start) - if delta > time.Second { - db.getDecoratedLogger(fn, extras).Warnf("dbatomic: tx took %v", delta) - } else if delta > time.Millisecond { - db.getDecoratedLogger(fn, extras).Debugf("dbatomic: tx took %v", delta) + if time.Now().After(atomicDeadline) { + db.getDecoratedLogger(fn, extras).Warnf("dbatomic: tx surpassed expected deadline by %v", atomicDeadline.Sub(time.Now())) + } + return +} + +// ResetTransactionWarnDeadline allow the atomic function to extend it's warn deadline by setting a new deadline. +// The Accessor can be copied and therefore isn't suitable for multi-threading directly, +// however, the transaction context and transaction object can be used to uniquely associate the request +// with a particular deadline. +// the function fails if the given transaction is not on the stack of the provided context. +func ResetTransactionWarnDeadline(ctx context.Context, tx *sql.Tx, deadline time.Time) (prevDeadline time.Time, err error) { + txContextData, ok := ctx.Value(tx).(*txExecutionContext) + if !ok { + // it's not a valid call. just return an error. + return time.Time{}, fmt.Errorf("the provided tx does not have a valid txExecutionContext object in it's context") } + prevDeadline = txContextData.deadline + txContextData.deadline = deadline return } @@ -362,7 +388,7 @@ func dbretry(obj error) bool { return ok && (err.Code == sqlite3.ErrLocked || err.Code == sqlite3.ErrBusy) } -type idemFn func(tx *sql.Tx) error +type idemFn func(ctx context.Context, tx *sql.Tx) error const infoTxRetries = 5 const warnTxRetriesInterval = 1 diff --git a/util/db/dbutil_test.go b/util/db/dbutil_test.go index 3c6e37d54c..ee043cecf0 100644 --- a/util/db/dbutil_test.go +++ b/util/db/dbutil_test.go @@ -17,6 +17,7 @@ package db import ( + "context" "database/sql" "errors" "fmt" @@ -31,18 +32,19 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/logging" ) func TestInMemoryDisposal(t *testing.T) { acc, err := MakeAccessor("fn.db", false, true) require.NoError(t, err) - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("create table Service (data blob)") return err }) require.NoError(t, err) - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { raw := []byte{0, 1, 2} _, err := tx.Exec("insert or replace into Service (rowid, data) values (1, ?)", raw) return err @@ -51,7 +53,7 @@ func TestInMemoryDisposal(t *testing.T) { anotherAcc, err := MakeAccessor("fn.db", false, true) require.NoError(t, err) - err = anotherAcc.Atomic(func(tx *sql.Tx) error { + err = anotherAcc.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int row := tx.QueryRow("select count(*) from Service") err := row.Scan(&nrows) @@ -64,7 +66,7 @@ func TestInMemoryDisposal(t *testing.T) { acc, err = MakeAccessor("fn.db", false, true) require.NoError(t, err) - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int row := tx.QueryRow("select count(*) from Service") err := row.Scan(&nrows) @@ -82,13 +84,13 @@ func TestInMemoryUniqueDB(t *testing.T) { acc, err := MakeAccessor("fn.db", false, true) require.NoError(t, err) defer acc.Close() - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("create table Service (data blob)") return err }) require.NoError(t, err) - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { raw := []byte{0, 1, 2} _, err := tx.Exec("insert or replace into Service (rowid, data) values (1, ?)", raw) return err @@ -98,7 +100,7 @@ func TestInMemoryUniqueDB(t *testing.T) { anotherAcc, err := MakeAccessor("fn2.db", false, true) require.NoError(t, err) defer anotherAcc.Close() - err = anotherAcc.Atomic(func(tx *sql.Tx) error { + err = anotherAcc.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int row := tx.QueryRow("select count(*) from Service") err := row.Scan(&nrows) @@ -122,13 +124,13 @@ func TestDBConcurrency(t *testing.T) { require.NoError(t, err) defer acc2.Close() - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("CREATE TABLE foo (a INTEGER, b INTEGER)") return err }) require.NoError(t, err) - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("INSERT INTO foo (a, b) VALUES (?, ?)", 1, 1) return err }) @@ -137,7 +139,7 @@ func TestDBConcurrency(t *testing.T) { c1 := make(chan struct{}) c2 := make(chan struct{}) go func() { - err := acc.Atomic(func(tx *sql.Tx) error { + err := acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { <-c2 _, err := tx.Exec("INSERT INTO foo (a, b) VALUES (?, ?)", 2, 2) @@ -160,7 +162,7 @@ func TestDBConcurrency(t *testing.T) { c1 <- struct{}{} }() - err = acc2.Atomic(func(tx *sql.Tx) error { + err = acc2.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int64 err := tx.QueryRow("SELECT COUNT(*) FROM foo").Scan(&nrows) if err != nil { @@ -178,7 +180,7 @@ func TestDBConcurrency(t *testing.T) { c2 <- struct{}{} <-c1 - err = acc2.Atomic(func(tx *sql.Tx) error { + err = acc2.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int64 err := tx.QueryRow("SELECT COUNT(*) FROM foo").Scan(&nrows) if err != nil { @@ -196,7 +198,7 @@ func TestDBConcurrency(t *testing.T) { c2 <- struct{}{} <-c1 - err = acc2.Atomic(func(tx *sql.Tx) error { + err = acc2.Atomic(func(ctx context.Context, tx *sql.Tx) error { var nrows int64 err := tx.QueryRow("SELECT COUNT(*) FROM foo").Scan(&nrows) if err != nil { @@ -249,7 +251,7 @@ func TestDBConcurrencyRW(t *testing.T) { require.NoError(t, err) defer acc2.Close() - err = acc.Atomic(func(tx *sql.Tx) error { + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("CREATE TABLE t (a INTEGER PRIMARY KEY)") return err }) @@ -266,7 +268,7 @@ func TestDBConcurrencyRW(t *testing.T) { }() var errw error for i, timedLoop := int64(1), true; timedLoop && errw == nil; i++ { - errw = acc.Atomic(func(tx *sql.Tx) error { + errw = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("INSERT INTO t (a) VALUES (?)", i) return err }) @@ -309,7 +311,7 @@ func TestDBConcurrencyRW(t *testing.T) { break } var x int64 - errsel := acc2.Atomic(func(tx *sql.Tx) error { + errsel := acc2.Atomic(func(ctx context.Context, tx *sql.Tx) error { return tx.QueryRow("SELECT a FROM t WHERE a=?", id).Scan(&x) }) if errsel != nil { @@ -333,3 +335,53 @@ func TestDBConcurrencyRW(t *testing.T) { } } + +type WarningLogCounter struct { + logging.Logger + warningsCounter int +} + +func (wlc *WarningLogCounter) Warnf(string, ...interface{}) { + wlc.warningsCounter++ +} + +func (wlc *WarningLogCounter) With(key string, value interface{}) logging.Logger { + return wlc +} + +// Test resetting warning notification +func TestResettingTransactionWarnDeadline(t *testing.T) { + t.Run("expectedWarning", func(t *testing.T) { + t.Parallel() + acc, err := MakeAccessor("fn-expectedWarning.db", false, true) + require.NoError(t, err) + defer acc.Close() + logger := WarningLogCounter{ + Logger: logging.Base(), + } + acc.log = &logger + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { + time.Sleep(1001 * time.Millisecond) + return err + }) + require.NoError(t, err) + require.Equal(t, 1, logger.warningsCounter) + }) + t.Run("expectedNoWarning", func(t *testing.T) { + t.Parallel() + acc, err := MakeAccessor("fn-expectedNoWarning.db", false, true) + require.NoError(t, err) + defer acc.Close() + logger := WarningLogCounter{ + Logger: logging.Base(), + } + acc.log = &logger + err = acc.Atomic(func(ctx context.Context, tx *sql.Tx) error { + ResetTransactionWarnDeadline(ctx, tx, time.Now().Add(30*time.Second)) + time.Sleep(1001 * time.Millisecond) + return err + }) + require.NoError(t, err) + require.Equal(t, 0, logger.warningsCounter) + }) +} diff --git a/util/db/perf_test.go b/util/db/perf_test.go index d10c386813..b58ef41427 100644 --- a/util/db/perf_test.go +++ b/util/db/perf_test.go @@ -38,7 +38,7 @@ func BenchmarkSQLWrites(b *testing.B) { logging.Base().SetLevel(logging.Error) - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("CREATE TABLE t (a integer primary key, b integer)") if err != nil { return err @@ -50,7 +50,7 @@ func BenchmarkSQLWrites(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("INSERT INTO t (a, b) VALUES (?, ?)", i, i) return err }) @@ -68,7 +68,7 @@ func BenchmarkSQLErasableWrites(b *testing.B) { logging.Base().SetLevel(logging.Error) - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("CREATE TABLE t (a integer primary key, b integer)") if err != nil { return err @@ -80,7 +80,7 @@ func BenchmarkSQLErasableWrites(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("INSERT INTO t (a, b) VALUES (?, ?)", i, i) return err }) @@ -99,7 +99,7 @@ func BenchmarkSQLQueryAPIs(b *testing.B) { logging.Base().SetLevel(logging.Error) - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { _, err := tx.Exec("CREATE TABLE t (a integer primary key, b integer)") if err != nil { return err @@ -111,7 +111,7 @@ func BenchmarkSQLQueryAPIs(b *testing.B) { b.Run("rdb.Atomic", func(b *testing.B) { for i := 0; i < b.N; i++ { - err = rdb.Atomic(func(tx *sql.Tx) error { + err = rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var b int err := tx.QueryRow("SELECT b FROM t WHERE a=?", i).Scan(&b) if err == sql.ErrNoRows { @@ -125,7 +125,7 @@ func BenchmarkSQLQueryAPIs(b *testing.B) { b.Run("wdb.Atomic", func(b *testing.B) { for i := 0; i < b.N; i++ { - err = wdb.Atomic(func(tx *sql.Tx) error { + err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var r int err := tx.QueryRow("SELECT b FROM t WHERE a=?", i).Scan(&r) if err == sql.ErrNoRows { @@ -138,7 +138,7 @@ func BenchmarkSQLQueryAPIs(b *testing.B) { }) b.Run("rdb.Atomic/Batch", func(b *testing.B) { - err = rdb.Atomic(func(tx *sql.Tx) error { + err = rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { for i := 0; i < b.N; i++ { var r int err := tx.QueryRow("SELECT b FROM t WHERE a=?", i).Scan(&r) @@ -152,7 +152,7 @@ func BenchmarkSQLQueryAPIs(b *testing.B) { }) b.Run("rdb.Atomic/PrepareBatch", func(b *testing.B) { - err = rdb.Atomic(func(tx *sql.Tx) error { + err = rdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { stmt, err := tx.Prepare("SELECT b FROM t WHERE a=?") if err != nil { return err From afdfd03d7c78eacc82a9bdd6d7deb6ef5534a9fc Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Thu, 16 Jul 2020 16:11:12 -0400 Subject: [PATCH 107/267] Fix tealdbg CDT url (#1252) Newer versions of Google Chrome dropped chrome-devtools:// schema and use devtools:// instead --- cmd/tealdbg/README.md | 2 +- cmd/tealdbg/cdtdbg.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/tealdbg/README.md b/cmd/tealdbg/README.md index bac158b6ad..b1fc841fcf 100644 --- a/cmd/tealdbg/README.md +++ b/cmd/tealdbg/README.md @@ -28,7 +28,7 @@ $ tealdbg debug samples/calls_count.teal --balance samples/calls_count_balance.json --txn samples/calls_count_txn.json --proto=future $ tealdbg debug samples/calls_count.teal --proto=future --painless ``` - It prints out the URL to follow: `chrome-devtools://devtools/bundled/js_app.html?...` + It prints out the URL to follow: `devtools://devtools/bundled/js_app.html?...` 2. Open the URL in Google Chrome. If you see the `This site can’t be reached` page, open Chrome DevTools [as explained](https://developers.google.com/web/tools/chrome-devtools/open) and refresh the page. diff --git a/cmd/tealdbg/cdtdbg.go b/cmd/tealdbg/cdtdbg.go index 2f7f720e38..acacc0703b 100644 --- a/cmd/tealdbg/cdtdbg.go +++ b/cmd/tealdbg/cdtdbg.go @@ -127,8 +127,8 @@ func (a *CDTAdapter) enableWebsocketEndpoint( Title: "Algorand TEAL program", TabType: "node", URL: "https://algorand.com/", - DevtoolsFrontendURL: "chrome-devtools://devtools/bundled/js_app.html?experiments=true&v8only=false&ws=" + address, - DevtoolsFrontendURLCompat: "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=false&ws=" + address, + DevtoolsFrontendURL: "devtools://devtools/bundled/js_app.html?experiments=true&v8only=false&ws=" + address, + DevtoolsFrontendURLCompat: "devtools://devtools/bundled/inspector.html?experiments=true&v8only=false&ws=" + address, WebSocketDebuggerURL: "ws://" + address, FaviconURL: "https://www.algorand.com/icons/icon-144x144.png", } From c0567f21d064eaa23af59c1fa9186d5886d534e7 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Thu, 16 Jul 2020 16:23:16 -0400 Subject: [PATCH 108/267] REST API: Merge ApplicationLocalStates and ApplicationLocalState. (#1250) Merge ApplicationLocalStates and ApplicationLocalState. --- daemon/algod/api/algod.oas2.json | 23 +- daemon/algod/api/algod.oas3.yml | 23 +- daemon/algod/api/server/v2/account.go | 22 +- daemon/algod/api/server/v2/account_test.go | 10 +- daemon/algod/api/server/v2/dryrun_test.go | 28 +- .../api/server/v2/generated/private/routes.go | 248 +++++++++--------- .../api/server/v2/generated/private/types.go | 13 +- .../algod/api/server/v2/generated/routes.go | 189 +++++++------ daemon/algod/api/server/v2/generated/types.go | 13 +- .../algod/api/server/v2/test/handlers_test.go | 12 +- daemon/algod/api/server/v2/test/helpers.go | 2 +- 11 files changed, 268 insertions(+), 315 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 7fc579faea..dff0548fb9 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1086,7 +1086,7 @@ "description": "\\[appl\\] applications local data stored in this account.\n\nNote the raw object uses `map[int] -\u003e AppLocalState` for this type.", "type": "array", "items": { - "$ref": "#/definitions/ApplicationLocalStates" + "$ref": "#/definitions/ApplicationLocalState" } }, "apps-total-schema": { @@ -1316,30 +1316,19 @@ } } }, - "ApplicationLocalStates": { - "description": "Pair of application index and application local state", - "type": "object", - "required": [ - "id", - "state" - ], - "properties": { - "id": { - "type": "integer" - }, - "state": { - "$ref": "#/definitions/ApplicationLocalState" - } - } - }, "ApplicationLocalState": { "description": "Stores local state associated with an application.", "type": "object", "required": [ + "id", "schema", "key-value" ], "properties": { + "id": { + "description": "The application which this local state is for.", + "type": "integer" + }, "schema": { "description": "\\[hsch\\] schema.", "$ref": "#/definitions/ApplicationStateSchema" diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 3c47d2fcec..34f148cf17 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -650,7 +650,7 @@ "apps-local-state": { "description": "\\[appl\\] applications local data stored in this account.\n\nNote the raw object uses `map[int] -> AppLocalState` for this type.", "items": { - "$ref": "#/components/schemas/ApplicationLocalStates" + "$ref": "#/components/schemas/ApplicationLocalState" }, "type": "array" }, @@ -800,6 +800,10 @@ "ApplicationLocalState": { "description": "Stores local state associated with an application.", "properties": { + "id": { + "description": "The application which this local state is for.", + "type": "integer" + }, "key-value": { "$ref": "#/components/schemas/TealKeyValueStore" }, @@ -808,27 +812,12 @@ } }, "required": [ + "id", "key-value", "schema" ], "type": "object" }, - "ApplicationLocalStates": { - "description": "Pair of application index and application local state", - "properties": { - "id": { - "type": "integer" - }, - "state": { - "$ref": "#/components/schemas/ApplicationLocalState" - } - }, - "required": [ - "id", - "state" - ], - "type": "object" - }, "ApplicationParams": { "description": "Stores the global information associated with an application.", "properties": { diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 72774516ef..4ce2aff73e 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -68,17 +68,15 @@ func AccountDataToAccount( createdApps = append(createdApps, app) } - appsLocalState := make([]generated.ApplicationLocalStates, 0, len(record.AppLocalStates)) + appsLocalState := make([]generated.ApplicationLocalState, 0, len(record.AppLocalStates)) for appIdx, state := range record.AppLocalStates { localState := convertTKVToGenerated(&state.KeyValue) - appsLocalState = append(appsLocalState, generated.ApplicationLocalStates{ - Id: uint64(appIdx), - State: generated.ApplicationLocalState{ - KeyValue: localState, - Schema: generated.ApplicationStateSchema{ - NumByteSlice: state.Schema.NumByteSlice, - NumUint: state.Schema.NumUint, - }, + appsLocalState = append(appsLocalState, generated.ApplicationLocalState{ + Id: uint64(appIdx), + KeyValue: localState, + Schema: generated.ApplicationStateSchema{ + NumByteSlice: state.Schema.NumByteSlice, + NumUint: state.Schema.NumUint, }, }) } @@ -219,10 +217,10 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { for _, ls := range *a.AppsLocalState { appLocalStates[basics.AppIndex(ls.Id)] = basics.AppLocalState{ Schema: basics.StateSchema{ - NumUint: ls.State.Schema.NumUint, - NumByteSlice: ls.State.Schema.NumByteSlice, + NumUint: ls.Schema.NumUint, + NumByteSlice: ls.Schema.NumByteSlice, }, - KeyValue: convertGeneratedTKV(&ls.State.KeyValue), + KeyValue: convertGeneratedTKV(&ls.KeyValue), } } } diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index 42d0be0a59..e2f4c286b1 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -74,9 +74,9 @@ func TestAccount(t *testing.T) { ls := (*conv.AppsLocalState)[0] require.Equal(t, uint64(appIdx), ls.Id) - require.Equal(t, uint64(10), ls.State.Schema.NumUint) - require.Equal(t, uint64(0), ls.State.Schema.NumByteSlice) - require.Equal(t, 2, len(ls.State.KeyValue)) + require.Equal(t, uint64(10), ls.Schema.NumUint) + require.Equal(t, uint64(0), ls.Schema.NumByteSlice) + require.Equal(t, 2, len(ls.KeyValue)) value1 := generated.TealKeyValue{ Key: "uint", Value: generated.TealValue{ @@ -91,8 +91,8 @@ func TestAccount(t *testing.T) { Bytes: "value", }, } - require.Contains(t, ls.State.KeyValue, value1) - require.Contains(t, ls.State.KeyValue, value2) + require.Contains(t, ls.KeyValue, value1) + require.Contains(t, ls.KeyValue, value2) c, err := AccountToAccountData(&conv) require.NoError(t, err) diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index 7e974a689e..ba80c7549f 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -511,7 +511,7 @@ func TestDryrunLocal1(t *testing.T) { { Status: "Online", Address: basics.Address{}.String(), - AppsLocalState: &[]generated.ApplicationLocalStates{{Id: 1}}, + AppsLocalState: &[]generated.ApplicationLocalState{{Id: 1}}, }, } doDryrunRequest(&dr, &proto, &response) @@ -581,7 +581,7 @@ func TestDryrunLocal1A(t *testing.T) { { Status: "Online", Address: basics.Address{}.String(), - AppsLocalState: &[]generated.ApplicationLocalStates{{Id: 1}}, + AppsLocalState: &[]generated.ApplicationLocalState{{Id: 1}}, }, } @@ -667,14 +667,10 @@ func TestDryrunLocalCheck(t *testing.T) { { Status: "Online", Address: basics.Address{}.String(), - AppsLocalState: &[]generated.ApplicationLocalStates{ - { - Id: 1, - State: generated.ApplicationLocalState{ - KeyValue: localv, - }, - }, - }, + AppsLocalState: &[]generated.ApplicationLocalState{{ + Id: 1, + KeyValue: localv, + }}, }, } @@ -723,14 +719,10 @@ func TestDryrunEncodeDecode(t *testing.T) { { Status: "Online", Address: basics.Address{}.String(), - AppsLocalState: &[]generated.ApplicationLocalStates{ - { - Id: 1, - State: generated.ApplicationLocalState{ - KeyValue: localv, - }, - }, - }, + AppsLocalState: &[]generated.ApplicationLocalState{{ + Id: 1, + KeyValue: localv, + }}, }, } diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 9a413eaa22..41976cbaf0 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -235,130 +235,130 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "Ap64t+9v/38AAAD//8igmd8zgwAA", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index ea3721b651..115b5c7040 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -23,7 +23,7 @@ type Account struct { // \[appl\] applications local data stored in this account. // // Note the raw object uses `map[int] -> AppLocalState` for this type. - AppsLocalState *[]ApplicationLocalStates `json:"apps-local-state,omitempty"` + AppsLocalState *[]ApplicationLocalState `json:"apps-local-state,omitempty"` // Specifies maximums on the number of each type that may be stored. AppsTotalSchema *ApplicationStateSchema `json:"apps-total-schema,omitempty"` @@ -114,6 +114,9 @@ type Application struct { // ApplicationLocalState defines model for ApplicationLocalState. type ApplicationLocalState struct { + // The application which this local state is for. + Id uint64 `json:"id"` + // Represents a key-value store for use in an application. KeyValue TealKeyValueStore `json:"key-value"` @@ -121,14 +124,6 @@ type ApplicationLocalState struct { Schema ApplicationStateSchema `json:"schema"` } -// ApplicationLocalStates defines model for ApplicationLocalStates. -type ApplicationLocalStates struct { - Id uint64 `json:"id"` - - // Stores local state associated with an application. - State ApplicationLocalState `json:"state"` -} - // ApplicationParams defines model for ApplicationParams. type ApplicationParams struct { diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index bc2cf5402e..ddf40cc5cb 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -620,101 +620,100 @@ var swaggerSpec = []string{ "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", - "AAAA///QLTg0r9cAAA==", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index dec25467c9..554f3d9b23 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -23,7 +23,7 @@ type Account struct { // \[appl\] applications local data stored in this account. // // Note the raw object uses `map[int] -> AppLocalState` for this type. - AppsLocalState *[]ApplicationLocalStates `json:"apps-local-state,omitempty"` + AppsLocalState *[]ApplicationLocalState `json:"apps-local-state,omitempty"` // Specifies maximums on the number of each type that may be stored. AppsTotalSchema *ApplicationStateSchema `json:"apps-total-schema,omitempty"` @@ -114,6 +114,9 @@ type Application struct { // ApplicationLocalState defines model for ApplicationLocalState. type ApplicationLocalState struct { + // The application which this local state is for. + Id uint64 `json:"id"` + // Represents a key-value store for use in an application. KeyValue TealKeyValueStore `json:"key-value"` @@ -121,14 +124,6 @@ type ApplicationLocalState struct { Schema ApplicationStateSchema `json:"schema"` } -// ApplicationLocalStates defines model for ApplicationLocalStates. -type ApplicationLocalStates struct { - Id uint64 `json:"id"` - - // Stores local state associated with an application. - State ApplicationLocalState `json:"state"` -} - // ApplicationParams defines model for ApplicationParams. type ApplicationParams struct { diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 17adf2a182..353bd573e8 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -453,14 +453,10 @@ func TestTealDryrun(t *testing.T) { gdr.Accounts = []generated.Account{ { Address: basics.Address{}.String(), - AppsLocalState: &[]generated.ApplicationLocalStates{ - { - Id: 1, - State: generated.ApplicationLocalState{ - KeyValue: localv, - }, - }, - }, + AppsLocalState: &[]generated.ApplicationLocalState{{ + Id: 1, + KeyValue: localv, + }}, }, } diff --git a/daemon/algod/api/server/v2/test/helpers.go b/daemon/algod/api/server/v2/test/helpers.go index 0435687158..34d12883c1 100644 --- a/daemon/algod/api/server/v2/test/helpers.go +++ b/daemon/algod/api/server/v2/test/helpers.go @@ -58,7 +58,7 @@ var cannedStatusReportGolden = node.StatusReport{ var poolAddrRewardBaseGolden = uint64(0) var poolAddrAssetsGolden = make([]generatedV2.AssetHolding, 0) var poolAddrCreatedAssetsGolden = make([]generatedV2.Asset, 0) -var appLocalStates = make([]generatedV2.ApplicationLocalStates, 0) +var appLocalStates = make([]generatedV2.ApplicationLocalState, 0) var appsTotalSchema = generatedV2.ApplicationStateSchema{} var appCreatedApps = make([]generatedV2.Application, 0) var poolAddrResponseGolden = generatedV2.AccountResponse{ From 1aba50216582404ab54a056d5f1ff75d0564101e Mon Sep 17 00:00:00 2001 From: "Ryan R. Fox" Date: Fri, 17 Jul 2020 15:18:33 -0400 Subject: [PATCH 109/267] Add generate-docs command (#1260) Add generate-docs command to `diagcfg` --- cmd/diagcfg/main.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/cmd/diagcfg/main.go b/cmd/diagcfg/main.go index 029e399144..fbd08552b8 100644 --- a/cmd/diagcfg/main.go +++ b/cmd/diagcfg/main.go @@ -21,6 +21,7 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" ) func init() { @@ -36,6 +37,17 @@ var rootCmd = &cobra.Command{ } func main() { + // Hidden command to generate docs in a given directory + // diagcfg generate-docs [path] + if len(os.Args) == 3 && os.Args[1] == "generate-docs" { + err := doc.GenMarkdownTree(rootCmd, os.Args[2]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) + } + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) From 66124403d7c0123504c3a32e20d392a0c0bc199c Mon Sep 17 00:00:00 2001 From: Will Winder Date: Fri, 17 Jul 2020 16:54:21 -0400 Subject: [PATCH 110/267] Don't mark optional field as required. --- daemon/algod/api/algod.oas2.json | 3 +- daemon/algod/api/algod.oas3.yml | 1 - daemon/algod/api/server/v2/account.go | 4 +- .../api/server/v2/generated/private/routes.go | 104 +++++------ .../api/server/v2/generated/private/types.go | 2 +- .../algod/api/server/v2/generated/routes.go | 162 +++++++++--------- daemon/algod/api/server/v2/generated/types.go | 2 +- 7 files changed, 138 insertions(+), 140 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index dff0548fb9..5c23d8102e 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1321,8 +1321,7 @@ "type": "object", "required": [ "id", - "schema", - "key-value" + "schema" ], "properties": { "id": { diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 34f148cf17..f9d0460a47 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -813,7 +813,6 @@ }, "required": [ "id", - "key-value", "schema" ], "type": "object" diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 4ce2aff73e..2f2f3489f7 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -73,7 +73,7 @@ func AccountDataToAccount( localState := convertTKVToGenerated(&state.KeyValue) appsLocalState = append(appsLocalState, generated.ApplicationLocalState{ Id: uint64(appIdx), - KeyValue: localState, + KeyValue: &localState, Schema: generated.ApplicationStateSchema{ NumByteSlice: state.Schema.NumByteSlice, NumUint: state.Schema.NumUint, @@ -220,7 +220,7 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { NumUint: ls.Schema.NumUint, NumByteSlice: ls.Schema.NumByteSlice, }, - KeyValue: convertGeneratedTKV(&ls.KeyValue), + KeyValue: convertGeneratedTKV(ls.KeyValue), } } } diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 41976cbaf0..644c447f85 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -307,58 +307,58 @@ var swaggerSpec = []string{ "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", - "Ap64t+9v/38AAAD//8igmd8zgwAA", + "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", + "9/b97f8PAAD//8QAO4wngwAA", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index 115b5c7040..9d62f9e774 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -118,7 +118,7 @@ type ApplicationLocalState struct { Id uint64 `json:"id"` // Represents a key-value store for use in an application. - KeyValue TealKeyValueStore `json:"key-value"` + KeyValue *TealKeyValueStore `json:"key-value,omitempty"` // Specifies maximums on the number of each type that may be stored. Schema ApplicationStateSchema `json:"schema"` diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index ddf40cc5cb..003f5390fe 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -633,87 +633,87 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index 554f3d9b23..1e849902f1 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -118,7 +118,7 @@ type ApplicationLocalState struct { Id uint64 `json:"id"` // Represents a key-value store for use in an application. - KeyValue TealKeyValueStore `json:"key-value"` + KeyValue *TealKeyValueStore `json:"key-value,omitempty"` // Specifies maximums on the number of each type that may be stored. Schema ApplicationStateSchema `json:"schema"` From c582d7391253c1ecb21001dc7db2b1ea39d9783c Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 17 Jul 2020 17:00:32 -0400 Subject: [PATCH 111/267] always reinsert block 0 on archival nodes to fix payset commitment issue --- data/ledger.go | 2 +- data/transactions/aggregates.go | 17 +++++++++- data/transactions/aggregates_test.go | 14 ++++++++ ledger/blockdb.go | 11 +++++++ ledger/blockdb_test.go | 48 ++++++++++++++++++++++++++++ ledger/ledger.go | 18 +++++++++-- 6 files changed, 106 insertions(+), 4 deletions(-) diff --git a/data/ledger.go b/data/ledger.go index 4b1a6b59b6..2b6eb4f688 100644 --- a/data/ledger.go +++ b/data/ledger.go @@ -72,7 +72,7 @@ func makeGenesisBlock(proto protocol.ConsensusVersion, genesisBal GenesisBalance Round: 0, Branch: bookkeeping.BlockHash{}, Seed: committee.Seed(genesisHash), - TxnRoot: transactions.Payset{}.Commit(params.PaysetCommitFlat), + TxnRoot: transactions.Payset{}.CommitGenesis(params.PaysetCommitFlat), TimeStamp: genesisBal.timestamp, GenesisID: genesisID, RewardsState: genesisRewardsState, diff --git a/data/transactions/aggregates.go b/data/transactions/aggregates.go index 8c676ba49f..3eb1bdf7ea 100644 --- a/data/transactions/aggregates.go +++ b/data/transactions/aggregates.go @@ -37,12 +37,27 @@ type ( // depends on the order in which the Txids appear, and that Txids do NOT cover // transaction signatures. func (payset Payset) Commit(flat bool) crypto.Digest { + return payset.commit(flat, false) +} + +// CommitGenesis is like Commit, but with special handling for zero-length +// but non-nil paysets. +func (payset Payset) CommitGenesis(flat bool) crypto.Digest { + return payset.commit(flat, true) +} + +// commit handles the logic for both Commit and CommitGenesis +func (payset Payset) commit(flat bool, genesis bool) crypto.Digest { // We used to build up Paysets from a nil slice with `append` during // block evaluation, meaning zero-length paysets would remain nil. // After we started allocating them up front, we started calling Commit // on zero-length but non-nil Paysets. However, we want payset // encodings to remain the same with or without this optimization. - if len(payset) == 0 { + // + // Additionally, the genesis block commits to a zero-length but non-nil + // payset (the only block to do so), so we have to let the nil value + // pass through. + if !genesis && len(payset) == 0 { payset = nil } diff --git a/data/transactions/aggregates_test.go b/data/transactions/aggregates_test.go index 87b67e9b5e..33bd618985 100644 --- a/data/transactions/aggregates_test.go +++ b/data/transactions/aggregates_test.go @@ -57,3 +57,17 @@ func TestPaysetDoesNotCommitToSignatures(t *testing.T) { commit2 := payset.Commit(false) require.Equal(t, commit1, commit2) } + +func TestEmptyPaysetCommitment(t *testing.T) { + const nilFlatPaysetHash = "WRS2VL2OQ5LPWBYLNBCZV3MEQ4DACSRDES6IUKHGOWYQERJRWC5A" + const emptyFlatPaysetHash = "E54GFMNS2LISPG5VUGOQ3B2RR7TRKAHRE24LUM3HOW6TJGQ6PNZQ" + + // Non-genesis blocks should encode empty paysets identically to nil paysets + var nilPayset Payset + require.Equal(t, nilFlatPaysetHash, Payset{}.Commit(true).String()) + require.Equal(t, nilFlatPaysetHash, nilPayset.Commit(true).String()) + + // Genesis block should encode empty payset differently + require.Equal(t, emptyFlatPaysetHash, Payset{}.CommitGenesis(true).String()) + require.Equal(t, nilFlatPaysetHash, nilPayset.CommitGenesis(true).String()) +} diff --git a/ledger/blockdb.go b/ledger/blockdb.go index b2c378024e..1a132929a3 100644 --- a/ledger/blockdb.go +++ b/ledger/blockdb.go @@ -144,6 +144,17 @@ func blockGetCert(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, cert agr return } +func blockReplaceIfExists(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) error { + _, err := tx.Exec("UPDATE blocks SET proto=?, hdrdata=?, blkdata=?, certdata=? WHERE rnd=?", + blk.CurrentProtocol, + protocol.Encode(&blk.BlockHeader), + protocol.Encode(&blk), + protocol.Encode(&cert), + blk.Round(), + ) + return err +} + func blockPut(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) error { var max sql.NullInt64 err := tx.QueryRow("SELECT MAX(rnd) FROM blocks").Scan(&max) diff --git a/ledger/blockdb_test.go b/ledger/blockdb_test.go index a7cd259e77..a5fd550843 100644 --- a/ledger/blockdb_test.go +++ b/ledger/blockdb_test.go @@ -25,9 +25,11 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/agreement" + "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" ) @@ -174,3 +176,49 @@ func TestBlockDBAppend(t *testing.T) { checkBlockDB(t, tx, blocks) } } + +func TestFixGenesisPaysetHash(t *testing.T) { + dbs, _ := dbOpenTest(t, true) + setDbLogging(t, dbs) + defer dbs.close() + + tx, err := dbs.wdb.Handle.Begin() + require.NoError(t, err) + defer tx.Rollback() + + // Fetch some consensus params + params := config.Consensus[protocol.ConsensusCurrentVersion] + + // Make a genesis block with a good payset hash + goodGenesis := randomBlock(basics.Round(0)) + goodGenesis.block.BlockHeader.TxnRoot = transactions.Payset{}.CommitGenesis(params.PaysetCommitFlat) + require.NoError(t, err) + + // Copy the genesis block and replace its payset hash with the buggy value + badGenesis := goodGenesis + badGenesis.block.BlockHeader.TxnRoot = transactions.Payset{}.Commit(params.PaysetCommitFlat) + + // Assert that the buggy value is different from the good value + require.NotEqual(t, goodGenesis.block.BlockHeader.TxnRoot, badGenesis.block.BlockHeader.TxnRoot) + + // Insert the buggy block + err = blockInit(tx, []bookkeeping.Block{badGenesis.block}) + require.NoError(t, err) + checkBlockDB(t, tx, []blockEntry{badGenesis}) + + // Check that it has the bad TxnRoot + blk, err := blockGet(tx, basics.Round(0)) + require.NoError(t, err) + require.Equal(t, blk.BlockHeader.TxnRoot, badGenesis.block.BlockHeader.TxnRoot) + + // Pretend to initBlocksDB for an archival node with the good genesis + l := &Ledger{log: logging.Base()} + err = initBlocksDB(tx, l, []bookkeeping.Block{goodGenesis.block}, true) + require.NoError(t, err) + checkBlockDB(t, tx, []blockEntry{goodGenesis}) + + // Check that it has the good TxnRoot + blk, err = blockGet(tx, basics.Round(0)) + require.NoError(t, err) + require.Equal(t, blk.BlockHeader.TxnRoot, goodGenesis.block.BlockHeader.TxnRoot) +} diff --git a/ledger/ledger.go b/ledger/ledger.go index 9a3908c804..00d5f7f481 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -235,10 +235,9 @@ func openLedgerDB(dbPathPrefix string, dbMem bool) (trackerDBs dbPair, blockDBs return } -// initLedgerDB performs DB initialization: +// initBlocksDB performs DB initialization: // - creates and populates it with genesis blocks // - ensures DB is in good shape for archival mode and resets it if not -// - does nothing if everything looks good func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchival bool) (err error) { err = blockInit(tx, initBlocks) if err != nil { @@ -269,7 +268,22 @@ func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchi return err } } + + // Manually replace block 0, even if we already had it + // (necessary to normalize the payset commitment because of a + // bug that caused its value to change) + // + // Don't bother for non-archival nodes since they will toss + // block 0 almost immediately + if len(initBlocks) > 0 && initBlocks[0].Round() == basics.Round(0) { + blockReplaceIfExists(tx, initBlocks[0], agreement.Certificate{}) + if err != nil { + err = fmt.Errorf("initBlocksDB.blockReplaceIfExists %v", err) + return err + } + } } + return nil } From 0979e8b502d3effc82bac984f33583cb844b479c Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 17 Jul 2020 17:04:44 -0400 Subject: [PATCH 112/267] add comment and missing error check --- ledger/ledger.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ledger/ledger.go b/ledger/ledger.go index 00d5f7f481..fcafd0a382 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -275,8 +275,11 @@ func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchi // // Don't bother for non-archival nodes since they will toss // block 0 almost immediately + // + // TODO remove this once a version containing this code has + // been deployed to archival nodes if len(initBlocks) > 0 && initBlocks[0].Round() == basics.Round(0) { - blockReplaceIfExists(tx, initBlocks[0], agreement.Certificate{}) + err = blockReplaceIfExists(tx, initBlocks[0], agreement.Certificate{}) if err != nil { err = fmt.Errorf("initBlocksDB.blockReplaceIfExists %v", err) return err From 004b7b210db2ab5bea2b67d98c33fb6037cdefb6 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 17 Jul 2020 17:16:48 -0400 Subject: [PATCH 113/267] fix typo --- data/transactions/aggregates_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/transactions/aggregates_test.go b/data/transactions/aggregates_test.go index 33bd618985..765fd506fa 100644 --- a/data/transactions/aggregates_test.go +++ b/data/transactions/aggregates_test.go @@ -67,7 +67,7 @@ func TestEmptyPaysetCommitment(t *testing.T) { require.Equal(t, nilFlatPaysetHash, Payset{}.Commit(true).String()) require.Equal(t, nilFlatPaysetHash, nilPayset.Commit(true).String()) - // Genesis block should encode empty payset differently + // Genesis block should encode the empty payset differently require.Equal(t, emptyFlatPaysetHash, Payset{}.CommitGenesis(true).String()) require.Equal(t, nilFlatPaysetHash, nilPayset.CommitGenesis(true).String()) } From 2d3634b84364e4c6ad02f3074aff05dbaff1932d Mon Sep 17 00:00:00 2001 From: Will Winder Date: Fri, 17 Jul 2020 17:20:18 -0400 Subject: [PATCH 114/267] Be specific with setting the key value store to nil when it's empty. --- daemon/algod/api/server/v2/account.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 2f2f3489f7..dc2b5a01d9 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -73,7 +73,7 @@ func AccountDataToAccount( localState := convertTKVToGenerated(&state.KeyValue) appsLocalState = append(appsLocalState, generated.ApplicationLocalState{ Id: uint64(appIdx), - KeyValue: &localState, + KeyValue: localState, Schema: generated.ApplicationStateSchema{ NumByteSlice: state.Schema.NumByteSlice, NumUint: state.Schema.NumUint, @@ -112,7 +112,12 @@ func AccountDataToAccount( }, nil } -func convertTKVToGenerated(tkv *basics.TealKeyValue) (converted generated.TealKeyValueStore) { +func convertTKVToGenerated(tkv *basics.TealKeyValue) *generated.TealKeyValueStore { + if (tkv == nil || len(*tkv) == 0) { + return nil + } + + var converted generated.TealKeyValueStore for k, v := range *tkv { converted = append(converted, generated.TealKeyValue{ Key: k, @@ -123,7 +128,7 @@ func convertTKVToGenerated(tkv *basics.TealKeyValue) (converted generated.TealKe }, }) } - return + return &converted } func convertGeneratedTKV(akvs *generated.TealKeyValueStore) basics.TealKeyValue { @@ -315,7 +320,7 @@ func AppParamsToApplication(creator string, appIdx basics.AppIndex, appParams *b Creator: creator, ApprovalProgram: appParams.ApprovalProgram, ClearStateProgram: appParams.ClearStateProgram, - GlobalState: &globalState, + GlobalState: globalState, LocalStateSchema: &generated.ApplicationStateSchema{ NumByteSlice: appParams.LocalStateSchema.NumByteSlice, NumUint: appParams.LocalStateSchema.NumUint, From 922d2078fccc5a591a528882b3855fafc3006add Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:56:27 -0400 Subject: [PATCH 115/267] Change constant values of DeltaAction (#1262) This change sets SetBytesAction = 1 and SetUintAction = 2 to keep things consistent --- daemon/algod/api/server/v2/dryrun_test.go | 20 ++++++++++---------- data/basics/teal.go | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index ba80c7549f..33b3ef810f 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -924,14 +924,14 @@ func TestDryrunRequestJSON(t *testing.T) { func TestStateDeltaToStateDelta(t *testing.T) { t.Parallel() sd := basics.StateDelta{ - "intkey": { - Action: basics.SetUintAction, - Uint: 11, - }, "byteskey": { Action: basics.SetBytesAction, Bytes: "test", }, + "intkey": { + Action: basics.SetUintAction, + Uint: 11, + }, "delkey": { Action: basics.DeleteAction, }, @@ -942,16 +942,16 @@ func TestStateDeltaToStateDelta(t *testing.T) { var keys []string // test with a loop because sd is a map and iteration order is random for _, item := range *gsd { - if item.Key == "intkey" { + if item.Key == "byteskey" { require.Equal(t, uint64(1), item.Value.Action) - require.NotNil(t, item.Value.Uint) - require.Equal(t, uint64(11), *item.Value.Uint) - require.Nil(t, item.Value.Bytes) - } else if item.Key == "byteskey" { - require.Equal(t, uint64(2), item.Value.Action) require.Nil(t, item.Value.Uint) require.NotNil(t, item.Value.Bytes) require.Equal(t, base64.StdEncoding.EncodeToString([]byte("test")), *item.Value.Bytes) + } else if item.Key == "intkey" { + require.Equal(t, uint64(2), item.Value.Action) + require.NotNil(t, item.Value.Uint) + require.Equal(t, uint64(11), *item.Value.Uint) + require.Nil(t, item.Value.Bytes) } else if item.Key == "delkey" { require.Equal(t, uint64(3), item.Value.Action) require.Nil(t, item.Value.Uint) diff --git a/data/basics/teal.go b/data/basics/teal.go index ca6c5d43b7..67945e3c7f 100644 --- a/data/basics/teal.go +++ b/data/basics/teal.go @@ -28,11 +28,11 @@ import ( type DeltaAction uint64 const ( - // SetUintAction indicates that a Uint should be stored at a key - SetUintAction DeltaAction = 1 - // SetBytesAction indicates that a TEAL byte slice should be stored at a key - SetBytesAction DeltaAction = 2 + SetBytesAction DeltaAction = 1 + + // SetUintAction indicates that a Uint should be stored at a key + SetUintAction DeltaAction = 2 // DeleteAction indicates that the value for a particular key should be deleted DeleteAction DeltaAction = 3 From 6e8858fb7ccb02a1d55d32494af0e01bceb8f2fa Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 17 Jul 2020 22:43:53 -0400 Subject: [PATCH 116/267] Deprecate support of a single database file We haven't been generating this database file from day 1, so now would be a good time to remove this legacy code. --- ledger/ledger.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/ledger/ledger.go b/ledger/ledger.go index 9a3908c804..efaffe9cf7 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -205,23 +205,20 @@ func openLedgerDB(dbPathPrefix string, dbMem bool) (trackerDBs dbPair, blockDBs var trackerDBFilename string var blockDBFilename string - commonDBFilename := dbPathPrefix + ".sqlite" if !dbMem { + commonDBFilename := dbPathPrefix + ".sqlite" _, err = os.Stat(commonDBFilename) + if !os.IsNotExist(err) { + // before launch, we used to have both blocks and tracker + // state in a single SQLite db file. We don't have that anymore, + // and we want to fail when that's the case. + err = fmt.Errorf("A single ledger database file '%s' was detected. This is no longer supported by current binary", commonDBFilename) + return + } } - if !dbMem && os.IsNotExist(err) { - // No common file, so use two separate files for blocks and tracker. - trackerDBFilename = dbPathPrefix + ".tracker.sqlite" - blockDBFilename = dbPathPrefix + ".block.sqlite" - } else if err == nil { - // Legacy common file exists (or testing in-memory, where performance - // doesn't matter), use same database for everything. - trackerDBFilename = commonDBFilename - blockDBFilename = commonDBFilename - } else { - return - } + trackerDBFilename = dbPathPrefix + ".tracker.sqlite" + blockDBFilename = dbPathPrefix + ".block.sqlite" trackerDBs, err = dbOpen(trackerDBFilename, dbMem) if err != nil { From de5b1b73531100d64643293d06a8bae68ce44c1b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 18 Jul 2020 09:35:44 -0400 Subject: [PATCH 117/267] Disable TestArchival unit test on linux-amd64 The unit test TestArchival is randomly failing on travis linux-amd64 configuration due to random memory allocation failures. --- ledger/archival_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 2019d4a9ff..ee1733565e 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -26,6 +26,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "testing" "github.com/stretchr/testify/require" @@ -107,6 +108,13 @@ func TestArchival(t *testing.T) { // We generate mostly empty blocks, with the exception of timestamps, // which affect participationTracker.committedUpTo()'s return value. + // This test was causing random crashes on travis when executed with the race detector + // due to memory exhustion. For the time being, I'm taking it offline from the ubuntu + // configuration where it used to cause failuires. + if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" { + t.Skip("Skipping the TestArchival as it tend to randomally fail on travis linux-amd64") + } + dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) genesisInitState := getInitState() const inMem = true From f286f79cc9319a4d6a2a050cd57891a8c40997a3 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Mon, 20 Jul 2020 09:05:14 -0400 Subject: [PATCH 118/267] Update tests for change, run make fmt. --- daemon/algod/api/server/v2/account.go | 2 +- daemon/algod/api/server/v2/account_test.go | 2 +- daemon/algod/api/server/v2/dryrun_test.go | 4 ++-- daemon/algod/api/server/v2/test/handlers_test.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index dc2b5a01d9..c0b94b5189 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -113,7 +113,7 @@ func AccountDataToAccount( } func convertTKVToGenerated(tkv *basics.TealKeyValue) *generated.TealKeyValueStore { - if (tkv == nil || len(*tkv) == 0) { + if tkv == nil || len(*tkv) == 0 { return nil } diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index e2f4c286b1..d73611f7d8 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -76,7 +76,7 @@ func TestAccount(t *testing.T) { require.Equal(t, uint64(appIdx), ls.Id) require.Equal(t, uint64(10), ls.Schema.NumUint) require.Equal(t, uint64(0), ls.Schema.NumByteSlice) - require.Equal(t, 2, len(ls.KeyValue)) + require.Equal(t, 2, len(*(ls.KeyValue))) value1 := generated.TealKeyValue{ Key: "uint", Value: generated.TealValue{ diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index ba80c7549f..58e91a766f 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -669,7 +669,7 @@ func TestDryrunLocalCheck(t *testing.T) { Address: basics.Address{}.String(), AppsLocalState: &[]generated.ApplicationLocalState{{ Id: 1, - KeyValue: localv, + KeyValue: &localv, }}, }, } @@ -721,7 +721,7 @@ func TestDryrunEncodeDecode(t *testing.T) { Address: basics.Address{}.String(), AppsLocalState: &[]generated.ApplicationLocalState{{ Id: 1, - KeyValue: localv, + KeyValue: &localv, }}, }, } diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index 353bd573e8..f9537b6226 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -455,7 +455,7 @@ func TestTealDryrun(t *testing.T) { Address: basics.Address{}.String(), AppsLocalState: &[]generated.ApplicationLocalState{{ Id: 1, - KeyValue: localv, + KeyValue: &localv, }}, }, } From fd057e00e1ae1aa9a03c1381b1684d9a75119923 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Mon, 20 Jul 2020 09:46:57 -0400 Subject: [PATCH 119/267] Another test fix. --- daemon/algod/api/server/v2/account_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index d73611f7d8..aed1147201 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -76,7 +76,7 @@ func TestAccount(t *testing.T) { require.Equal(t, uint64(appIdx), ls.Id) require.Equal(t, uint64(10), ls.Schema.NumUint) require.Equal(t, uint64(0), ls.Schema.NumByteSlice) - require.Equal(t, 2, len(*(ls.KeyValue))) + require.Equal(t, 2, len(*ls.KeyValue)) value1 := generated.TealKeyValue{ Key: "uint", Value: generated.TealValue{ @@ -91,8 +91,8 @@ func TestAccount(t *testing.T) { Bytes: "value", }, } - require.Contains(t, ls.KeyValue, value1) - require.Contains(t, ls.KeyValue, value2) + require.Contains(t, *ls.KeyValue, value1) + require.Contains(t, *ls.KeyValue, value2) c, err := AccountToAccountData(&conv) require.NoError(t, err) From 6b2149fb77237596e23a03f25ee161a8dbac5f5a Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 20 Jul 2020 11:06:17 -0400 Subject: [PATCH 120/267] log if replacing block 0 replaces unexpected data --- data/transactions/aggregates_test.go | 8 ++++ ledger/blockdb.go | 59 ++++++++++++++++++++++++---- ledger/ledger.go | 5 ++- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/data/transactions/aggregates_test.go b/data/transactions/aggregates_test.go index 765fd506fa..663790f793 100644 --- a/data/transactions/aggregates_test.go +++ b/data/transactions/aggregates_test.go @@ -61,6 +61,7 @@ func TestPaysetDoesNotCommitToSignatures(t *testing.T) { func TestEmptyPaysetCommitment(t *testing.T) { const nilFlatPaysetHash = "WRS2VL2OQ5LPWBYLNBCZV3MEQ4DACSRDES6IUKHGOWYQERJRWC5A" const emptyFlatPaysetHash = "E54GFMNS2LISPG5VUGOQ3B2RR7TRKAHRE24LUM3HOW6TJGQ6PNZQ" + const merklePaysetHash = "4OYMIQUY7QOBJGX36TEJS35ZEQT24QPEMSNZGTFESWMRW6CSXBKQ" // Non-genesis blocks should encode empty paysets identically to nil paysets var nilPayset Payset @@ -70,4 +71,11 @@ func TestEmptyPaysetCommitment(t *testing.T) { // Genesis block should encode the empty payset differently require.Equal(t, emptyFlatPaysetHash, Payset{}.CommitGenesis(true).String()) require.Equal(t, nilFlatPaysetHash, nilPayset.CommitGenesis(true).String()) + + // Non-flat paysets (which we have dropped support for) should encode + // the same regardless of nilness or if this is a genesis block + require.Equal(t, merklePaysetHash, Payset{}.CommitGenesis(false).String()) + require.Equal(t, merklePaysetHash, nilPayset.CommitGenesis(false).String()) + require.Equal(t, merklePaysetHash, Payset{}.Commit(false).String()) + require.Equal(t, merklePaysetHash, nilPayset.Commit(false).String()) } diff --git a/ledger/blockdb.go b/ledger/blockdb.go index 1a132929a3..284f9a1990 100644 --- a/ledger/blockdb.go +++ b/ledger/blockdb.go @@ -17,6 +17,7 @@ package ledger import ( + "bytes" "database/sql" "fmt" "strings" @@ -26,6 +27,7 @@ import ( "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" ) @@ -144,15 +146,58 @@ func blockGetCert(tx *sql.Tx, rnd basics.Round) (blk bookkeeping.Block, cert agr return } -func blockReplaceIfExists(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) error { - _, err := tx.Exec("UPDATE blocks SET proto=?, hdrdata=?, blkdata=?, certdata=? WHERE rnd=?", - blk.CurrentProtocol, - protocol.Encode(&blk.BlockHeader), - protocol.Encode(&blk), - protocol.Encode(&cert), +func blockReplaceIfExists(tx *sql.Tx, log logging.Logger, blk bookkeeping.Block, cert agreement.Certificate) (updated bool, err error) { + // Fetch encoded block + cert for the requested round so we can compare + var oldProto protocol.ConsensusVersion + var oldHdr, oldBlk, oldCert []byte + const query = "SELECT proto, hdrdata, blkdata, certdata FROM blocks WHERE rnd=?" + err = tx.QueryRow(query, blk.Round()).Scan(&oldProto, &oldHdr, &oldBlk, &oldCert) + if err != nil { + // Didn't have a block to replace, no problem + if err == sql.ErrNoRows { + return false, nil + } + return false, err + } + + // Encode new block + cert in order to check against old values and replace + newProto := blk.CurrentProtocol + newHdr := protocol.Encode(&blk.BlockHeader) + newBlk := protocol.Encode(&blk) + newCert := protocol.Encode(&cert) + + // Log if protocol version or certificate changed for the block we're replacing + if newProto != oldProto { + log.Warnf("blockReplaceIfExists(%v): old proto %v != new proto %v", blk.Round(), oldProto, newProto) + } + if !bytes.Equal(oldCert, newCert) { + log.Warnf("blockReplaceIfExists(%v): old cert %v != new cert %v", blk.Round(), oldCert, newCert) + } + + // Replace the block + res, err := tx.Exec("UPDATE blocks SET proto=?, hdrdata=?, blkdata=?, certdata=? WHERE rnd=?", + newProto, + newHdr, + newBlk, + newCert, blk.Round(), ) - return err + if err != nil { + return false, err + } + + // Ensure we actually updated a row + cnt, err := res.RowsAffected() + if err != nil { + return true, err + } + if cnt > 0 { + return true, nil + } + + // Shouldn't get here since we found the block + log.Warnf("blockReplaceIfExists(%v): found block but didn't update any rows?", blk.Round()) + return false, nil } func blockPut(tx *sql.Tx, blk bookkeeping.Block, cert agreement.Certificate) error { diff --git a/ledger/ledger.go b/ledger/ledger.go index fcafd0a382..b23b21da42 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -279,11 +279,14 @@ func initBlocksDB(tx *sql.Tx, l *Ledger, initBlocks []bookkeeping.Block, isArchi // TODO remove this once a version containing this code has // been deployed to archival nodes if len(initBlocks) > 0 && initBlocks[0].Round() == basics.Round(0) { - err = blockReplaceIfExists(tx, initBlocks[0], agreement.Certificate{}) + updated, err := blockReplaceIfExists(tx, l.log, initBlocks[0], agreement.Certificate{}) if err != nil { err = fmt.Errorf("initBlocksDB.blockReplaceIfExists %v", err) return err } + if updated { + l.log.Infof("initBlocksDB replaced block 0") + } } } From b364bdfc214fd6cbcfa6b4e49803d3eceae16eae Mon Sep 17 00:00:00 2001 From: John Lee Date: Mon, 20 Jul 2020 16:14:46 -0400 Subject: [PATCH 121/267] Add duplicate check on ephemeral security group and key pair creation for buildhost #659 (#1258) The buildhost may fail if the security group or key pair is pre-existing. This may happen if startup fails, as the old security groups and key pairs were not cleaned up with errors. This will also result in instances left running on the account. This change will check for pre-existing security groups and key pairs, and re-generate the INSTANCE_ID if either exist. The fall through error condition was also modified to run the shutdown script, to delete the security group, key pair, and instance. --- scripts/buildhost/shutdown_ec2_instance.sh | 17 +++-- scripts/buildhost/start_ec2_instance.sh | 76 ++++++++++++++-------- 2 files changed, 57 insertions(+), 36 deletions(-) diff --git a/scripts/buildhost/shutdown_ec2_instance.sh b/scripts/buildhost/shutdown_ec2_instance.sh index 3884468cc0..4783866e3a 100755 --- a/scripts/buildhost/shutdown_ec2_instance.sh +++ b/scripts/buildhost/shutdown_ec2_instance.sh @@ -21,15 +21,14 @@ fi SGID=$(cat sgid) INSTANCE_ID=$(cat instance-id) -INSTANCE_NAME=$(cat instance) KEY_NAME=$(cat key-name) echo "Waiting for instance to terminate" end=$((SECONDS+1200)) while [ $SECONDS -lt $end ]; do - aws ec2 terminate-instances --instance-ids ${INSTANCE_ID} --region ${AWS_REGION} > instance2.json - INSTANCE_CODE=$(cat instance2.json | jq '.TerminatingInstances[].CurrentState.Code') - INSTANCE_STATE=$(cat instance2.json | jq '.TerminatingInstances[].CurrentState.Name') + aws ec2 terminate-instances --instance-ids "${INSTANCE_ID}" --region "${AWS_REGION}" > instance2.json + INSTANCE_CODE=$(jq '.TerminatingInstances[].CurrentState.Code' < instance2.json) + INSTANCE_STATE=$(jq '.TerminatingInstances[].CurrentState.Name' < instance2.json) if [ "${INSTANCE_CODE}" = "48" ]; then echo "Instance terminated" break @@ -40,9 +39,9 @@ while [ $SECONDS -lt $end ]; do cat instance2.json fi sleep 5s - aws ec2 describe-instance-status --instance-id ${INSTANCE_ID} --region ${AWS_REGION} --include-all-instances > instance2.json - INSTANCE_CODE=$(cat instance2.json | jq '.InstanceStatuses[].InstanceState.Code') - INSTANCE_STATE=$(cat instance2.json | jq '.InstanceStatuses[].InstanceState.Name') + aws ec2 describe-instance-status --instance-id "${INSTANCE_ID}" --region "${AWS_REGION}" --include-all-instances > instance2.json + INSTANCE_CODE=$(jq '.InstanceStatuses[].InstanceState.Code' < instance2.json) + INSTANCE_STATE=$(jq '.InstanceStatuses[].InstanceState.Name' < instance2.json) if [ "${INSTANCE_CODE}" = "48" ]; then echo "Instance terminated" break @@ -56,11 +55,11 @@ while [ $SECONDS -lt $end ]; do done if [ "${KEY_NAME}" != "" ]; then - aws ec2 delete-key-pair --key-name "${KEY_NAME}" --region ${AWS_REGION} + aws ec2 delete-key-pair --key-name "${KEY_NAME}" --region "${AWS_REGION}" fi if [ "${SGID}" != "" ]; then - aws ec2 delete-security-group --group-id "${SGID}" --region ${AWS_REGION} + aws ec2 delete-security-group --group-id "${SGID}" --region "${AWS_REGION}" fi rm instance2.json sgid instance-id instance key-name diff --git a/scripts/buildhost/start_ec2_instance.sh b/scripts/buildhost/start_ec2_instance.sh index 6f734acd48..52460cd417 100755 --- a/scripts/buildhost/start_ec2_instance.sh +++ b/scripts/buildhost/start_ec2_instance.sh @@ -8,7 +8,7 @@ # # Exit Code: returns 0 if instance started successfully, non-zero otherwise # -# Examples: scripts/buildhost/start_ec2_instance.sh +# Examples: scripts/buildhost/start_ec2_instance.sh # # Upon successfull execution, the following files would be created: # sgid - contain the security group identifier @@ -16,6 +16,8 @@ # instance - contains the address of the created instance # +set -eo pipefail + AWS_REGION=$1 AWS_AMI=$2 AWS_INSTANCE_TYPE=$3 @@ -23,48 +25,67 @@ INSTANCE_NUMBER=$RANDOM KEY_NAME="BuilderInstanceKey_${INSTANCE_NUMBER}" SECURITY_GROUP_NAME="BuilderMachineSSH_${INSTANCE_NUMBER}" -SGID=$(aws ec2 create-security-group --group-name ${SECURITY_GROUP_NAME} --description "Security Group for ephermal build machine to allow port 22" --region ${AWS_REGION} | jq -r '.GroupId') -if [ "$?" != "0" ]; then +function security_group_exists { + if aws ec2 describe-security-groups --group-name "${SECURITY_GROUP_NAME}" --region "$AWS_REGION" > /dev/null 2>&1; then + echo "WARNING: security group ${SECURITY_GROUP_NAME} exists" + return 0 + fi + + return 1 +} + +function key_pair_exists { + if aws ec2 describe-key-pairs --key-names "${KEY_NAME}" --region "$AWS_REGION" > /dev/null 2>&1; then + echo "WARNING: key pair ${KEY_NAME} exists" + return 0 + fi + return 1 +} + +while security_group_exists || key_pair_exists; do + echo "Selecting new random instance number" + INSTANCE_NUMBER=$RANDOM + KEY_NAME="BuilderInstanceKey_${INSTANCE_NUMBER}" + SECURITY_GROUP_NAME="BuilderMachineSSH_${INSTANCE_NUMBER}" +done + +if ! SGID=$(aws ec2 create-security-group --group-name ${SECURITY_GROUP_NAME} --description "Security Group for ephemeral build machine to allow port 22" --region "${AWS_REGION}" | jq -r '.GroupId') ; then exit 1 fi -aws ec2 authorize-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 22 --cidr 0.0.0.0/0 --region ${AWS_REGION} -if [ "$?" != "0" ]; then - aws ec2 delete-security-group --group-id "${SGID}" --region ${AWS_REGION} +if ! aws ec2 authorize-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 22 --cidr 0.0.0.0/0 --region "${AWS_REGION}" ; then + aws ec2 delete-security-group --group-id "${SGID}" --region "${AWS_REGION}" exit 1 fi -aws ec2 authorize-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 5022 --cidr 0.0.0.0/0 --region ${AWS_REGION} -if [ "$?" != "0" ]; then - aws ec2 delete-security-group --group-id "${SGID}" --region ${AWS_REGION} +if ! aws ec2 authorize-security-group-ingress --group-name ${SECURITY_GROUP_NAME} --protocol tcp --port 5022 --cidr 0.0.0.0/0 --region "${AWS_REGION}" ; then + aws ec2 delete-security-group --group-id "${SGID}" --region "${AWS_REGION}" exit 1 fi rm -f key.pem -aws ec2 create-key-pair --key-name "${KEY_NAME}" --region ${AWS_REGION} | jq -r '.KeyMaterial' > key.pem -if [ "$?" != "0" ]; then - aws ec2 delete-security-group --group-id "${SGID}" --region ${AWS_REGION} +if ! aws ec2 create-key-pair --key-name "${KEY_NAME}" --region "${AWS_REGION}" | jq -r '.KeyMaterial' > key.pem ; then + aws ec2 delete-security-group --group-id "${SGID}" --region "${AWS_REGION}" rm key.pem exit 1 fi -aws ec2 run-instances --image-id ${AWS_AMI} --key-name "${KEY_NAME}" --security-groups ${SECURITY_GROUP_NAME} --instance-type "${AWS_INSTANCE_TYPE}" --tag-specifications "ResourceType=instance,Tags=[{Key=\"Name\",Value=\"Buildhost_Ephermal_Instance_${INSTANCE_NUMBER}\"}, {Key=\"For\",Value=\"Buildhost_Ephermal_Instance\"}]" --block-device-mappings DeviceName=/dev/sdh,Ebs={VolumeSize=100} --count 1 --region ${AWS_REGION} > instance.json -if [ "$?" != "0" ]; then - aws ec2 delete-key-pair --key-name "${KEY_NAME}" --region ${AWS_REGION} - aws ec2 delete-security-group --group-id "${SGID}" --region ${AWS_REGION} +if ! aws ec2 run-instances --image-id "${AWS_AMI}" --key-name "${KEY_NAME}" --security-groups ${SECURITY_GROUP_NAME} --instance-type "${AWS_INSTANCE_TYPE}" --tag-specifications "ResourceType=instance,Tags=[{Key=\"Name\",Value=\"Buildhost_Ephermal_Instance_${INSTANCE_NUMBER}\"}, {Key=\"For\",Value=\"Buildhost_Ephermal_Instance\"}]" --block-device-mappings DeviceName="/dev/sdh,Ebs={VolumeSize=100}" --count 1 --region "${AWS_REGION}" > instance.json ; then + aws ec2 delete-key-pair --key-name "${KEY_NAME}" --region "${AWS_REGION}" + aws ec2 delete-security-group --group-id "${SGID}" --region "${AWS_REGION}" rm key.pem exit 1 fi -INSTANCE_ID=$(cat instance.json | jq -r '.Instances[].InstanceId') +INSTANCE_ID=$(jq -r '.Instances[].InstanceId' < instance.json) echo "Waiting for instance to start" end=$((SECONDS+90)) while [ $SECONDS -lt $end ]; do - aws ec2 describe-instance-status --instance-id ${INSTANCE_ID} --region ${AWS_REGION} --include-all-instances > instance2.json - INSTANCE_CODE=$(cat instance2.json | jq '.InstanceStatuses[].InstanceState.Code') - INSTANCE_STATE=$(cat instance2.json | jq '.InstanceStatuses[].InstanceState.Name') + aws ec2 describe-instance-status --instance-id "${INSTANCE_ID}" --region "${AWS_REGION}" --include-all-instances > instance2.json + INSTANCE_CODE=$(jq '.InstanceStatuses[].InstanceState.Code' < instance2.json) + INSTANCE_STATE=$(jq '.InstanceStatuses[].InstanceState.Name' < instance2.json) if [ "${INSTANCE_CODE}" = "16" ]; then echo "Instance started" break @@ -77,8 +98,8 @@ while [ $SECONDS -lt $end ]; do sleep 1s done -aws ec2 describe-instances --region ${AWS_REGION} --instance-id ${INSTANCE_ID} > instance2.json -INSTANCE_NAME=$(cat instance2.json | jq -r '.Reservations[].Instances[].PublicDnsName') +aws ec2 describe-instances --region "${AWS_REGION}" --instance-id "${INSTANCE_ID}" > instance2.json +INSTANCE_NAME=$(jq -r '.Reservations[].Instances[].PublicDnsName' < instance2.json) echo "Instance name = ${INSTANCE_NAME}" rm instance.json instance2.json @@ -93,8 +114,7 @@ chmod 400 key.pem echo "Waiting for SSH connection" end=$((SECONDS+90)) while [ $SECONDS -lt $end ]; do - ssh -i key.pem -o "StrictHostKeyChecking no" ubuntu@$(cat instance) "uname" - if [ "$?" = "0" ]; then + if ssh -i key.pem -o "StrictHostKeyChecking no" ubuntu@"$(cat instance)" "uname" ; then echo "SSH connection ready" exit 0 fi @@ -102,6 +122,8 @@ while [ $SECONDS -lt $end ]; do done echo "error: Unable to establish SSH connection" -rm -f key.pem -rm sgid instance instance-id key-name -exit 1 \ No newline at end of file + +# Fallthrough error condition - run shutdown to delete security group, keypair, and instance +"$(cd "$(dirname "$0")" && pwd)"/shutdown_ec2_instance.sh "$AWS_REGION" + +exit 1 From 1a066d3dd1e4d2e0e9a7c0e69efa1909c869a18c Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Mon, 20 Jul 2020 17:34:48 -0400 Subject: [PATCH 122/267] Improve dryrun and tealdbg compatibility (#1264) tealdbg now uses DryrunRequest.Sources if supplied --- cmd/tealdbg/dryrunRequest.go | 21 ++++++------- cmd/tealdbg/local.go | 22 +++++++++++++- daemon/algod/api/server/v2/dryrun.go | 45 +++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/cmd/tealdbg/dryrunRequest.go b/cmd/tealdbg/dryrunRequest.go index 482a47afe5..e8c05e1f12 100644 --- a/cmd/tealdbg/dryrunRequest.go +++ b/cmd/tealdbg/dryrunRequest.go @@ -79,21 +79,22 @@ func balanceRecordsFromDdr(ddr *v2.DryrunRequest) (records []basics.BalanceRecor if err != nil { return } - appIdx := basics.AppIndex(a.Id) - var ad basics.AccountData - var ok bool - if ad, ok = accounts[addr]; ok { - // skip if this app params are already set - if _, ok = ad.AppParams[appIdx]; ok { - continue - } - } // deserialize app params and update account data params := v2.ApplicationParamsToAppParams(&a.Params) + appIdx := basics.AppIndex(a.Id) + ad := accounts[addr] if ad.AppParams == nil { ad.AppParams = make(map[basics.AppIndex]basics.AppParams, 1) + ad.AppParams[appIdx] = params + } else { + ap, ok := ad.AppParams[appIdx] + if ok { + v2.MergeAppParams(&ap, ¶ms) + ad.AppParams[appIdx] = ap + } else { + ad.AppParams[appIdx] = params + } } - ad.AppParams[appIdx] = params accounts[addr] = ad } diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index 35c60b8a63..bf22c7417b 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -230,7 +230,19 @@ func determineEvalMode(program []byte, modeIn string) (eval evalFn, mode string, return } -// Setup validates input params +// Setup validates input params and resolves inputs into canonical balance record structures. +// Programs for execution are discovered in the following way: +// - Sources from command line file names. +// - Programs mentioned in transaction group txnGroup. +// - if DryrunRequest present and no sources or transaction group set in command line then: +// 1. DryrunRequest.Sources are expanded to DryrunRequest.Apps or DryrunRequest.Txns. +// 2. DryrunRequest.Apps are expanded into DryrunRequest.Txns. +// 3. txnGroup is set to DryrunRequest.Txns +// Application search by id: +// - Balance records from CLI or DryrunRequest.Accounts +// - If no balance records set in CLI then DryrunRequest.Accounts and DryrunRequest.Apps are used. +// In this case Accounts data is used as a base for balance records creation, +// and Apps supply updates to AppParams field. func (r *LocalRunner) Setup(dp *DebugParams) (err error) { ddr, err := ddrFromParams(dp) if err != nil { @@ -256,6 +268,14 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { } } + // if no sources provided, check dryrun request object + if len(dp.ProgramBlobs) == 0 && len(ddr.Sources) > 0 { + err = ddr.ExpandSources() + if err != nil { + return + } + } + var records []basics.BalanceRecord if len(dp.BalanceBlob) > 0 { records, err = balanceRecordsFromParams(dp) diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 3d5ef4129f..c7e78854b5 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -78,7 +78,9 @@ func DryrunRequestFromGenerated(gdr *generated.DryrunRequest) (dr DryrunRequest, return } -func (dr *DryrunRequest) expandSources() error { +// ExpandSources takes DryrunRequest.Source, compiles and +// puts into appropriate DryrunRequest.Apps entry +func (dr *DryrunRequest) ExpandSources() error { for i, s := range dr.Sources { program, err := logic.AssembleString(s.Source) if err != nil { @@ -287,8 +289,19 @@ func (dl *dryrunLedger) Get(addr basics.Address, withPendingRewards bool) (basic if ok { any = true app := dl.dr.Apps[appi] - out.AppParams = make(map[basics.AppIndex]basics.AppParams) - out.AppParams[basics.AppIndex(app.Id)] = ApplicationParamsToAppParams(&app.Params) + params := ApplicationParamsToAppParams(&app.Params) + if out.AppParams == nil { + out.AppParams = make(map[basics.AppIndex]basics.AppParams) + out.AppParams[basics.AppIndex(app.Id)] = params + } else { + ap, ok := out.AppParams[basics.AppIndex(app.Id)] + if ok { + MergeAppParams(&ap, ¶ms) + out.AppParams[basics.AppIndex(app.Id)] = ap + } else { + out.AppParams[basics.AppIndex(app.Id)] = params + } + } } if !any { return basics.BalanceRecord{}, fmt.Errorf("no account for addr %s", addr.String()) @@ -369,8 +382,13 @@ func makeAppLedger(dl *dryrunLedger, txn *transactions.Transaction, appIdx basic } // unit-testable core of dryrun handler +// programs for execution are discovered in the following way: +// - LogicSig: stxn.Lsig.Logic +// - Application: Apps[i].ClearStateProgram or Apps[i].ApprovalProgram for matched appIdx +// if DryrunRequest.Sources is set it overrides appropriate entires in stxn.Lsig.Logic or Apps[i] +// important: Accounts are not used for program lookup for application execution func doDryrunRequest(dr *DryrunRequest, proto *config.ConsensusParams, response *generated.DryrunResponse) { - err := dr.expandSources() + err := dr.ExpandSources() if err != nil { response.Error = err.Error() return @@ -512,3 +530,22 @@ func StateDeltaToStateDelta(sd basics.StateDelta) *generated.StateDelta { return &gsd } + +// MergeAppParams merges values, existing in "base" take priority over new in "update" +func MergeAppParams(base *basics.AppParams, update *basics.AppParams) { + if len(base.ApprovalProgram) == 0 && len(update.ApprovalProgram) > 0 { + base.ApprovalProgram = update.ApprovalProgram + } + if len(base.ClearStateProgram) == 0 && len(update.ClearStateProgram) > 0 { + base.ClearStateProgram = update.ClearStateProgram + } + if len(base.GlobalState) == 0 && len(update.GlobalState) > 0 { + base.GlobalState = update.GlobalState + } + if base.LocalStateSchema == (basics.StateSchema{}) && update.LocalStateSchema != (basics.StateSchema{}) { + base.LocalStateSchema = update.LocalStateSchema + } + if base.GlobalStateSchema == (basics.StateSchema{}) && update.GlobalStateSchema != (basics.StateSchema{}) { + base.GlobalStateSchema = update.GlobalStateSchema + } +} From 1fd3f80d9cfa1f195ef40d603c5a5fc45652a4e2 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 11:28:02 -0400 Subject: [PATCH 123/267] Fix negative "tx surpassed expected deadline" messages (#1273) --- util/db/dbutil.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/db/dbutil.go b/util/db/dbutil.go index f6aa00cdc6..9ff43f0aa8 100644 --- a/util/db/dbutil.go +++ b/util/db/dbutil.go @@ -340,7 +340,7 @@ func (db *Accessor) atomic(fn idemFn, commitLocker sync.Locker, extras ...interf } if time.Now().After(atomicDeadline) { - db.getDecoratedLogger(fn, extras).Warnf("dbatomic: tx surpassed expected deadline by %v", atomicDeadline.Sub(time.Now())) + db.getDecoratedLogger(fn, extras).Warnf("dbatomic: tx surpassed expected deadline by %v", time.Now().Sub(atomicDeadline)) } return } From 579293b12b6fe487cdd589484351c90839028a8e Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 14:29:57 -0400 Subject: [PATCH 124/267] Add blocks header to the header cache (#1275) The typical flow is that we add a new block, and then try to evaluate the subsequent block. When doing so, the previous block was already flushed to disk by the block.syncer, and so we're reloading the block from the blockqueue just so we can store it's header in the ledger.headerCache. --- ledger/ledger.go | 1 + ledger/ledger_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/ledger/ledger.go b/ledger/ledger.go index efaffe9cf7..9a4d0b8b68 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -477,6 +477,7 @@ func (l *Ledger) AddValidatedBlock(vb ValidatedBlock, cert agreement.Certificate if err != nil { return err } + l.headerCache.Put(vb.blk.Round(), vb.blk.BlockHeader) l.trackers.newBlock(vb.blk, vb.delta) return nil } diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index c3e37d1821..01894cc455 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -862,3 +862,28 @@ func testLedgerRegressionFaultyLeaseFirstValidCheck2f3880f7(t *testing.T, versio a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "should allow leasing payment transaction with newer FirstValid") } } + +func TestLedgerBlockHdrCaching(t *testing.T) { + dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) + genesisInitState := getInitState() + const inMem = true + cfg := config.GetDefaultLocal() + cfg.Archival = true + log := logging.TestingLog(t) + l, err := OpenLedger(log, dbName, inMem, genesisInitState, cfg) + require.NoError(t, err) + defer l.Close() + + blk := genesisInitState.Block + + for i := 0; i < 2000; i++ { + blk.BlockHeader.Round++ + blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) + err := l.AddBlock(blk, agreement.Certificate{}) + require.NoError(t, err) + + hdr, err := l.BlockHdr(blk.BlockHeader.Round) + require.NoError(t, err) + require.Equal(t, blk.BlockHeader, hdr) + } +} From 76ac4f634fe4e14da5680709aac8d1daf4c4aae9 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 14:46:19 -0400 Subject: [PATCH 125/267] Add reencoding database accounts support (#1251) This PR Implements the accounts reencoding on first upgrade of the database. It also introduces the usage of versioning and adjusting the atomic call warning deadline adjustments. --- ledger/accountdb.go | 117 +++++++++++++++++++++++++++- ledger/accountdb_test.go | 112 +++++++++++++++++++++++++++ ledger/acctupdates.go | 154 +++++++++++++++++++++++++++++++++++-- ledger/acctupdates_test.go | 43 +++++++++++ ledger/catchpointwriter.go | 2 +- ledger/catchupaccessor.go | 7 ++ 6 files changed, 424 insertions(+), 11 deletions(-) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index b21d1e3b73..5ccb77caf6 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -17,9 +17,11 @@ package ledger import ( + "bytes" "context" "database/sql" "fmt" + "time" "github.com/mattn/go-sqlite3" @@ -95,6 +97,8 @@ var accountsResetExprs = []string{ `DROP TABLE IF EXISTS accounthashes`, } +var accountDBVersion = int32(2) + type accountDelta struct { old basics.AccountData new basics.AccountData @@ -274,7 +278,8 @@ func accountsReset(tx *sql.Tx) error { return err } } - return nil + _, err := db.SetUserVersion(context.Background(), tx, 0) + return err } // accountsRound returns the tracker balances round number, and the round of the hash tree @@ -524,6 +529,28 @@ func (qs *accountsDbQueries) writeCatchpointStateString(ctx context.Context, sta return cleared, err } +func (qs *accountsDbQueries) close() { + preparedQueries := []**sql.Stmt{ + &qs.listCreatablesStmt, + &qs.lookupStmt, + &qs.lookupCreatorStmt, + &qs.deleteStoredCatchpoint, + &qs.insertStoredCatchpoint, + &qs.selectOldestsCatchpointFiles, + &qs.selectCatchpointStateUint64, + &qs.deleteCatchpointState, + &qs.insertCatchpointStateUint64, + &qs.selectCatchpointStateString, + &qs.insertCatchpointStateString, + } + for _, preparedQuery := range preparedQueries { + if (*preparedQuery) != nil { + (*preparedQuery).Close() + *preparedQuery = nil + } + } +} + func accountsAll(tx *sql.Tx) (bals map[basics.Address]basics.AccountData, err error) { rows, err := tx.Query("SELECT address, data FROM accountbase") if err != nil { @@ -707,8 +734,8 @@ func updateAccountsRound(tx *sql.Tx, rnd basics.Round, hashRound basics.Round) ( // encodedAccountsRange returns an array containing the account data, in the same way it appear in the database // starting at entry startAccountIndex, and up to accountCount accounts long. -func encodedAccountsRange(tx *sql.Tx, startAccountIndex, accountCount int) (bals []encodedBalanceRecord, err error) { - rows, err := tx.Query("SELECT address, data FROM accountbase ORDER BY rowid LIMIT ? OFFSET ?", accountCount, startAccountIndex) +func encodedAccountsRange(ctx context.Context, tx *sql.Tx, startAccountIndex, accountCount int) (bals []encodedBalanceRecord, err error) { + rows, err := tx.QueryContext(ctx, "SELECT address, data FROM accountbase ORDER BY rowid LIMIT ? OFFSET ?", accountCount, startAccountIndex) if err != nil { return } @@ -735,6 +762,14 @@ func encodedAccountsRange(tx *sql.Tx, startAccountIndex, accountCount int) (bals } err = rows.Err() + if err == nil { + // the encodedAccountsRange typically called in a loop iterating over all the accounts. This could clearly take more than the + // "standard" 1 second, so we want to extend the timeout on each iteration. If the last iteration takes more than a second, then + // it should be noted. The one second here is quite liberal to ensure symmetrical behaviour on low-power devices. + // The return value from ResetTransactionWarnDeadline can be safely ignored here since it would only default to writing the warnning + // message, which would let us know that it failed anyway. + db.ResetTransactionWarnDeadline(ctx, tx, time.Now().Add(time.Second)) + } return } @@ -749,6 +784,82 @@ func totalAccounts(ctx context.Context, tx *sql.Tx) (total uint64, err error) { return } +// reencodeAccounts reads all the accounts in the accountbase table, decode and reencode the account data. +// if the account data is found to have a different encoding, it would update the encoded account on disk. +// on return, it returns the number of modified accounts as well as an error ( if we had any ) +func reencodeAccounts(ctx context.Context, tx *sql.Tx) (modifiedAccounts uint, err error) { + modifiedAccounts = 0 + scannedAccounts := 0 + + updateStmt, err := tx.PrepareContext(ctx, "UPDATE accountbase SET data = ? WHERE address = ?") + if err != nil { + return 0, err + } + + rows, err := tx.QueryContext(ctx, "SELECT address, data FROM accountbase") + if err != nil { + return + } + defer rows.Close() + + var addr basics.Address + for rows.Next() { + // once every 1000 accounts we scan through, update the warning deadline. + // as long as the last "chunk" takes less than one second, we should be good to go. + // note that we should be quite liberal on timing here, since it might perform much slower + // on low-power devices. + if scannedAccounts%1000 == 0 { + // The return value from ResetTransactionWarnDeadline can be safely ignored here since it would only default to writing the warnning + // message, which would let us know that it failed anyway. + db.ResetTransactionWarnDeadline(ctx, tx, time.Now().Add(time.Second)) + } + + var addrbuf []byte + var preencodedAccountData []byte + err = rows.Scan(&addrbuf, &preencodedAccountData) + if err != nil { + return + } + + if len(addrbuf) != len(addr) { + err = fmt.Errorf("Account DB address length mismatch: %d != %d", len(addrbuf), len(addr)) + return + } + copy(addr[:], addrbuf[:]) + scannedAccounts++ + + // decode and re-encode: + var decodedAccountData basics.AccountData + err = protocol.Decode(preencodedAccountData, &decodedAccountData) + if err != nil { + return + } + reencodedAccountData := protocol.Encode(&decodedAccountData) + if bytes.Compare(preencodedAccountData, reencodedAccountData) == 0 { + // these are identical, no need to store re-encoded account data + continue + } + + // we need to update the encoded data. + result, err := updateStmt.ExecContext(ctx, reencodedAccountData, addrbuf) + if err != nil { + return 0, err + } + rowsUpdated, err := result.RowsAffected() + if err != nil { + return 0, err + } + if rowsUpdated != 1 { + return 0, fmt.Errorf("failed to update account %v, number of rows updated was %d instead of 1", addr, rowsUpdated) + } + modifiedAccounts++ + } + + err = rows.Err() + updateStmt.Close() + return +} + // merkleCommitterNodesPerPage controls how many nodes will be stored in a single page // value was calibrated using BenchmarkCalibrateNodesPerPage var merkleCommitterNodesPerPage = int64(116) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index a7a51b5b78..b331a4db18 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -17,7 +17,9 @@ package ledger import ( + "context" "database/sql" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -126,6 +128,7 @@ func checkAccounts(t *testing.T, tx *sql.Tx, rnd basics.Round, accts map[basics. aq, err := accountsDbInit(tx, tx) require.NoError(t, err) + defer aq.close() var totalOnline, totalOffline, totalNotPart uint64 @@ -278,3 +281,112 @@ func BenchmarkReadingAllBalances(b *testing.B) { } require.Equal(b, b.N, len(bal)) } + +func TestAccountsReencoding(t *testing.T) { + oldEncodedAccountsData := [][]byte{ + {132, 164, 97, 108, 103, 111, 206, 5, 234, 236, 80, 164, 97, 112, 97, 114, 129, 206, 0, 3, 60, 164, 137, 162, 97, 109, 196, 32, 49, 54, 101, 102, 97, 97, 51, 57, 50, 52, 97, 54, 102, 100, 57, 100, 51, 97, 52, 56, 50, 52, 55, 57, 57, 97, 52, 97, 99, 54, 53, 100, 162, 97, 110, 167, 65, 80, 84, 75, 73, 78, 71, 162, 97, 117, 174, 104, 116, 116, 112, 58, 47, 47, 115, 111, 109, 101, 117, 114, 108, 161, 99, 196, 32, 183, 97, 139, 76, 1, 45, 180, 52, 183, 186, 220, 252, 85, 135, 185, 87, 156, 87, 158, 83, 49, 200, 133, 169, 43, 205, 26, 148, 50, 121, 28, 105, 161, 102, 196, 32, 183, 97, 139, 76, 1, 45, 180, 52, 183, 186, 220, 252, 85, 135, 185, 87, 156, 87, 158, 83, 49, 200, 133, 169, 43, 205, 26, 148, 50, 121, 28, 105, 161, 109, 196, 32, 60, 69, 244, 159, 234, 26, 168, 145, 153, 184, 85, 182, 46, 124, 227, 144, 84, 113, 176, 206, 109, 204, 245, 165, 100, 23, 71, 49, 32, 242, 146, 68, 161, 114, 196, 32, 183, 97, 139, 76, 1, 45, 180, 52, 183, 186, 220, 252, 85, 135, 185, 87, 156, 87, 158, 83, 49, 200, 133, 169, 43, 205, 26, 148, 50, 121, 28, 105, 161, 116, 205, 3, 32, 162, 117, 110, 163, 65, 80, 75, 165, 97, 115, 115, 101, 116, 129, 206, 0, 3, 60, 164, 130, 161, 97, 0, 161, 102, 194, 165, 101, 98, 97, 115, 101, 205, 98, 54}, + {132, 164, 97, 108, 103, 111, 206, 5, 230, 217, 88, 164, 97, 112, 97, 114, 129, 206, 0, 3, 60, 175, 137, 162, 97, 109, 196, 32, 49, 54, 101, 102, 97, 97, 51, 57, 50, 52, 97, 54, 102, 100, 57, 100, 51, 97, 52, 56, 50, 52, 55, 57, 57, 97, 52, 97, 99, 54, 53, 100, 162, 97, 110, 167, 65, 80, 84, 75, 105, 110, 103, 162, 97, 117, 174, 104, 116, 116, 112, 58, 47, 47, 115, 111, 109, 101, 117, 114, 108, 161, 99, 196, 32, 111, 157, 243, 205, 146, 155, 167, 149, 44, 226, 153, 150, 6, 105, 206, 72, 182, 218, 38, 146, 98, 94, 57, 205, 145, 152, 12, 60, 175, 149, 94, 13, 161, 102, 196, 32, 111, 157, 243, 205, 146, 155, 167, 149, 44, 226, 153, 150, 6, 105, 206, 72, 182, 218, 38, 146, 98, 94, 57, 205, 145, 152, 12, 60, 175, 149, 94, 13, 161, 109, 196, 32, 60, 69, 244, 159, 234, 26, 168, 145, 153, 184, 85, 182, 46, 124, 227, 144, 84, 113, 176, 206, 109, 204, 245, 165, 100, 23, 71, 49, 32, 242, 146, 68, 161, 114, 196, 32, 111, 157, 243, 205, 146, 155, 167, 149, 44, 226, 153, 150, 6, 105, 206, 72, 182, 218, 38, 146, 98, 94, 57, 205, 145, 152, 12, 60, 175, 149, 94, 13, 161, 116, 205, 1, 44, 162, 117, 110, 164, 65, 80, 84, 75, 165, 97, 115, 115, 101, 116, 130, 206, 0, 3, 56, 153, 130, 161, 97, 10, 161, 102, 194, 206, 0, 3, 60, 175, 130, 161, 97, 0, 161, 102, 194, 165, 101, 98, 97, 115, 101, 205, 98, 54}, + {131, 164, 97, 108, 103, 111, 206, 5, 233, 179, 208, 165, 97, 115, 115, 101, 116, 130, 206, 0, 3, 60, 164, 130, 161, 97, 2, 161, 102, 194, 206, 0, 3, 60, 175, 130, 161, 97, 30, 161, 102, 194, 165, 101, 98, 97, 115, 101, 205, 98, 54}, + {131, 164, 97, 108, 103, 111, 206, 0, 3, 48, 104, 165, 97, 115, 115, 101, 116, 129, 206, 0, 1, 242, 159, 130, 161, 97, 0, 161, 102, 194, 165, 101, 98, 97, 115, 101, 205, 98, 54}, + } + dbs, _ := dbOpenTest(t, true) + setDbLogging(t, dbs) + defer dbs.close() + + secrets := crypto.GenerateOneTimeSignatureSecrets(15, 500) + pubVrfKey, _ := crypto.VrfKeygenFromSeed([32]byte{0, 1, 2, 3}) + + err := dbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + err = accountsInit(tx, make(map[basics.Address]basics.AccountData), config.Consensus[protocol.ConsensusCurrentVersion]) + if err != nil { + return err + } + + for _, oldAccData := range oldEncodedAccountsData { + addr := randomAddress() + _, err = tx.ExecContext(ctx, "INSERT INTO accountbase (address, data) VALUES (?, ?)", addr[:], oldAccData) + if err != nil { + return err + } + } + for i := 0; i < 100; i++ { + addr := randomAddress() + accData := basics.AccountData{ + MicroAlgos: basics.MicroAlgos{Raw: 0x000ffffffffffffff}, + Status: basics.NotParticipating, + RewardsBase: uint64(i), + RewardedMicroAlgos: basics.MicroAlgos{Raw: 0x000ffffffffffffff}, + VoteID: secrets.OneTimeSignatureVerifier, + SelectionID: pubVrfKey, + VoteFirstValid: basics.Round(0x000ffffffffffffff), + VoteLastValid: basics.Round(0x000ffffffffffffff), + VoteKeyDilution: 0x000ffffffffffffff, + AssetParams: map[basics.AssetIndex]basics.AssetParams{ + 0x000ffffffffffffff: basics.AssetParams{ + Total: 0x000ffffffffffffff, + Decimals: 0x2ffffff, + DefaultFrozen: true, + UnitName: "12345678", + AssetName: "12345678901234567890123456789012", + URL: "12345678901234567890123456789012", + MetadataHash: pubVrfKey, + Manager: addr, + Reserve: addr, + Freeze: addr, + Clawback: addr, + }, + }, + Assets: map[basics.AssetIndex]basics.AssetHolding{ + 0x000ffffffffffffff: basics.AssetHolding{ + Amount: 0x000ffffffffffffff, + Frozen: true, + }, + }, + } + + _, err = tx.ExecContext(ctx, "INSERT INTO accountbase (address, data) VALUES (?, ?)", addr[:], protocol.Encode(&accData)) + if err != nil { + return err + } + } + return nil + }) + require.NoError(t, err) + + err = dbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + modifiedAccounts, err := reencodeAccounts(ctx, tx) + if err != nil { + return err + } + if len(oldEncodedAccountsData) != int(modifiedAccounts) { + return fmt.Errorf("len(oldEncodedAccountsData) != int(modifiedAccounts) %d != %d", len(oldEncodedAccountsData), int(modifiedAccounts)) + } + require.Equal(t, len(oldEncodedAccountsData), int(modifiedAccounts)) + return nil + }) + require.NoError(t, err) +} + +// TestAccountsDbQueriesCreateClose tests to see that we can create the accountsDbQueries and close it. +// it also verify that double-closing it doesn't create an issue. +func TestAccountsDbQueriesCreateClose(t *testing.T) { + dbs, _ := dbOpenTest(t, true) + setDbLogging(t, dbs) + defer dbs.close() + + err := dbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { + err = accountsInit(tx, make(map[basics.Address]basics.AccountData), config.Consensus[protocol.ConsensusCurrentVersion]) + if err != nil { + return err + } + return nil + }) + require.NoError(t, err) + qs, err := accountsDbInit(dbs.rdb.Handle, dbs.wdb.Handle) + require.NoError(t, err) + require.NotNil(t, qs.listCreatablesStmt) + qs.close() + require.Nil(t, qs.listCreatablesStmt) + qs.close() + require.Nil(t, qs.listCreatablesStmt) +} diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index c4df4328ae..6ecb56e393 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -38,6 +38,7 @@ import ( "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/logging/telemetryspec" "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/util/db" ) const ( @@ -697,7 +698,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo lastestBlockRound = l.Latest() err = au.dbs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { var err0 error - au.dbRound, err0 = au.accountsInitialize(tx) + au.dbRound, err0 = au.accountsInitialize(ctx, tx) if err0 != nil { return err0 } @@ -708,7 +709,7 @@ func (au *accountUpdates) initializeFromDisk(l ledgerForTracker) (lastBalancesRo if err0 != nil { return err0 } - au.dbRound, err0 = au.accountsInitialize(tx) + au.dbRound, err0 = au.accountsInitialize(ctx, tx) if err0 != nil { return err0 } @@ -776,10 +777,42 @@ func accountHashBuilder(addr basics.Address, accountData basics.AccountData, enc } // Initialize accounts DB if needed and return account round -func (au *accountUpdates) accountsInitialize(tx *sql.Tx) (basics.Round, error) { - err := accountsInit(tx, au.initAccounts, au.initProto) +func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (basics.Round, error) { + // check current database version. + dbVersion, err := db.GetUserVersion(ctx, tx) if err != nil { - return 0, err + return 0, fmt.Errorf("accountsInitialize unable to read database schema version : %v", err) + } + + // if database version is greater than supported by current binary, write a warning. This would keep the existing + // fallback behaviour where we could use an older binary iff the schema happen to be backward compatible. + if dbVersion > accountDBVersion { + au.log.Warnf("accountsInitialize database schema version is %d, but algod supports only %d", dbVersion, accountDBVersion) + } + + if dbVersion < accountDBVersion { + au.log.Infof("accountsInitialize upgrading database schema from version %d to version %d", dbVersion, accountDBVersion) + + for dbVersion < accountDBVersion { + au.log.Infof("accountsInitialize performing upgrade from version %d", dbVersion) + // perform the initialization/upgrade + switch dbVersion { + case 0: + dbVersion, err = au.upgradeDatabaseSchema0(ctx, tx) + if err != nil { + return 0, err + } + case 1: + dbVersion, err = au.upgradeDatabaseSchema1(ctx, tx) + if err != nil { + return 0, err + } + default: + return 0, fmt.Errorf("accountsInitialize unable to upgrade database from schema version %d", dbVersion) + } + } + + au.log.Infof("accountsInitialize database schema upgrade complete") } rnd, hashRound, err := accountsRound(tx) @@ -819,7 +852,7 @@ func (au *accountUpdates) accountsInitialize(tx *sql.Tx) (basics.Round, error) { if rootHash.IsZero() { accountIdx := 0 for { - bal, err := encodedAccountsRange(tx, accountIdx, trieRebuildAccountChunkSize) + bal, err := encodedAccountsRange(ctx, tx, accountIdx, trieRebuildAccountChunkSize) if err != nil { return rnd, err } @@ -838,7 +871,7 @@ func (au *accountUpdates) accountsInitialize(tx *sql.Tx) (basics.Round, error) { return rnd, fmt.Errorf("accountsInitialize was unable to add changes to trie: %v", err) } if !added { - au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie.", hash) + au.log.Warnf("accountsInitialize attempted to add duplicate hash '%v' to merkle trie.", hash) } } @@ -853,11 +886,118 @@ func (au *accountUpdates) accountsInitialize(tx *sql.Tx) (basics.Round, error) { } accountIdx += trieRebuildAccountChunkSize } + + // we've just updated the markle trie, update the hashRound to reflect that. + err = updateAccountsRound(tx, rnd, rnd) + if err != nil { + return 0, fmt.Errorf("accountsInitialize was unable to update the account round to %d: %v", rnd, err) + } } au.balancesTrie = trie return rnd, nil } +// upgradeDatabaseSchema0 upgrades the database schema from version 0 to version 1 +func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { + au.log.Infof("accountsInitialize initializing schema") + err = accountsInit(tx, au.initAccounts, au.initProto) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to initialize schema : %v", err) + } + _, err = db.SetUserVersion(ctx, tx, 1) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to update database schema version from 0 to 1: %v", err) + } + return 1, nil +} + +// upgradeDatabaseSchema1 upgrades the database schema from version 1 to version 2 +func (au *accountUpdates) upgradeDatabaseSchema1(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { + // update accounts encoding. + au.log.Infof("accountsInitialize verifying accounts data encoding") + modifiedAccounts, err := reencodeAccounts(ctx, tx) + if err != nil { + return 0, err + } + + if modifiedAccounts > 0 { + au.log.Infof("accountsInitialize reencoded %d accounts", modifiedAccounts) + + au.log.Infof("accountsInitialize resetting account hashes") + // reset the merkle trie + err = resetAccountHashes(tx) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to reset account hashes : %v", err) + } + + au.log.Infof("accountsInitialize preparing queries") + // initialize a new accountsq with the incoming transaction. + accountsq, err := accountsDbInit(tx, tx) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to prepare queries : %v", err) + } + + // close the prepared statements when we're done with them. + defer accountsq.close() + + au.log.Infof("accountsInitialize resetting prior catchpoints") + // delete the last catchpoint label if we have any. + _, err = accountsq.writeCatchpointStateString(ctx, catchpointStateLastCatchpoint, "") + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to clear prior catchpoint : %v", err) + } + + au.log.Infof("accountsInitialize deleting stored catchpoints") + // delete catchpoints. + err = au.deleteStoredCatchpoints(ctx, accountsq) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to delete stored catchpoints : %v", err) + } + } else { + au.log.Infof("accountsInitialize found that no accounts needed to be reencoded") + } + + // update version + _, err = db.SetUserVersion(ctx, tx, 2) + if err != nil { + return 0, fmt.Errorf("accountsInitialize unable to update database schema version from 1 to 2: %v", err) + } + return 2, nil +} + +// deleteStoredCatchpoints iterates over the storedcatchpoints table and deletes all the files stored on disk. +// once all the files have been deleted, it would go ahead and remove the entries from the table. +func (au *accountUpdates) deleteStoredCatchpoints(ctx context.Context, dbQueries *accountsDbQueries) (err error) { + catchpointsFilesChunkSize := 50 + for { + fileNames, err := dbQueries.getOldestCatchpointFiles(ctx, catchpointsFilesChunkSize, 0) + if err != nil { + return err + } + if len(fileNames) == 0 { + break + } + + for round, fileName := range fileNames { + absCatchpointFileName := filepath.Join(au.dbDirectory, fileName) + err = os.Remove(absCatchpointFileName) + if err == nil || os.IsNotExist(err) { + // it's ok if the file doesn't exist. just remove it from the database and we'll be good to go. + err = nil + } else { + // we can't delete the file, abort - + return fmt.Errorf("unable to delete old catchpoint file '%s' : %v", absCatchpointFileName, err) + } + // clear the entry from the database + err = dbQueries.storeCatchpoint(ctx, round, "", "", 0) + if err != nil { + return err + } + } + } + return nil +} + func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Address]accountDelta) (err error) { if au.catchpointInterval == 0 { return nil diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index bec76282bb..2653f2f1c3 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -785,3 +785,46 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { inMemory = false t.Run("DiskDB", testFunction) } + +func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { + proto := config.Consensus[protocol.ConsensusCurrentVersion] + + ml := makeMockLedgerForTracker(t, true) + defer ml.close() + ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) + + accts := []map[basics.Address]basics.AccountData{randomAccounts(20)} + au := &accountUpdates{} + conf := config.GetDefaultLocal() + conf.CatchpointInterval = 1 + au.initialize(conf, ".", proto, accts[0]) + defer au.close() + + err := au.loadFromDisk(ml) + require.NoError(t, err) + + dummyCatchpointFilesToCreate := 42 + + for i := 0; i < dummyCatchpointFilesToCreate; i++ { + f, err := os.Create(fmt.Sprintf("./dummy_catchpoint_file-%d", i)) + require.NoError(t, err) + err = f.Close() + require.NoError(t, err) + } + + for i := 0; i < dummyCatchpointFilesToCreate; i++ { + err := au.accountsq.storeCatchpoint(context.Background(), basics.Round(i), fmt.Sprintf("./dummy_catchpoint_file-%d", i), "", 0) + require.NoError(t, err) + } + err = au.deleteStoredCatchpoints(context.Background(), au.accountsq) + require.NoError(t, err) + + for i := 0; i < dummyCatchpointFilesToCreate; i++ { + // ensure that all the files were deleted. + _, err := os.Open(fmt.Sprintf("./dummy_catchpoint_file-%d", i)) + require.True(t, os.IsNotExist(err)) + } + fileNames, err := au.accountsq.getOldestCatchpointFiles(context.Background(), dummyCatchpointFilesToCreate, 0) + require.NoError(t, err) + require.Equal(t, 0, len(fileNames)) +} diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index 0d059f655d..8fbd531853 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -222,7 +222,7 @@ func (cw *catchpointWriter) WriteStep(ctx context.Context) (more bool, err error } func (cw *catchpointWriter) readDatabaseStep(ctx context.Context, tx *sql.Tx) (err error) { - cw.balancesChunk.Balances, err = encodedAccountsRange(tx, cw.balancesOffset, BalancesPerCatchpointFileChunk) + cw.balancesChunk.Balances, err = encodedAccountsRange(ctx, tx, cw.balancesOffset, BalancesPerCatchpointFileChunk) if err == nil { cw.balancesOffset += BalancesPerCatchpointFileChunk } diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index 53c76d54b9..a321d3056b 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -183,6 +183,7 @@ func (c *CatchpointCatchupAccessorImpl) ResetStagingBalances(ctx context.Context if err != nil { return fmt.Errorf("unable to initialize accountsDbInit: %v", err) } + defer sq.close() _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBalancesRound, 0) if err != nil { return err @@ -252,6 +253,7 @@ func (c *CatchpointCatchupAccessorImpl) processStagingContent(ctx context.Contex if err != nil { return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to initialize accountsDbInit: %v", err) } + defer sq.close() _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBlockRound, uint64(fileHeader.BlocksRound)) if err != nil { return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingContent: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBlockRound, err) @@ -425,6 +427,10 @@ func (c *CatchpointCatchupAccessorImpl) StoreBalancesRound(ctx context.Context, wdb := c.ledger.trackerDB().wdb err = wdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { sq, err := accountsDbInit(tx, tx) + if err != nil { + return fmt.Errorf("CatchpointCatchupAccessorImpl::StoreBalancesRound: unable to initialize accountsDbInit: %v", err) + } + defer sq.close() _, err = sq.writeCatchpointStateUint64(ctx, catchpointStateCatchupBalancesRound, uint64(balancesRound)) if err != nil { return fmt.Errorf("CatchpointCatchupAccessorImpl::StoreBalancesRound: unable to write catchpoint catchup state '%s': %v", catchpointStateCatchupBalancesRound, err) @@ -512,6 +518,7 @@ func (c *CatchpointCatchupAccessorImpl) finishBalances(ctx context.Context) (err if err != nil { return fmt.Errorf("unable to initialize accountsDbInit: %v", err) } + defer sq.close() balancesRound, _, err = sq.readCatchpointStateUint64(ctx, catchpointStateCatchupBalancesRound) if err != nil { From 6290ada52fde1b0246f449a7e382877648e22c2f Mon Sep 17 00:00:00 2001 From: algomaxj <65551122+algomaxj@users.noreply.github.com> Date: Tue, 21 Jul 2020 15:15:25 -0400 Subject: [PATCH 126/267] Ensure `ApplyData.EvalDelta` is clear if `applyEvalDelta` fails (#1267) If StatefulEval returns an invalid EvalDelta for a ClearStateProgram, the current code would still fill in ApplyData with the EvalDelta, which it shouldn't do. --- data/transactions/application.go | 84 ++++++++++++-------------- data/transactions/application_test.go | 87 +++++++++++++-------------- 2 files changed, 80 insertions(+), 91 deletions(-) diff --git a/data/transactions/application.go b/data/transactions/application.go index da2c96c8cc..58f35299ed 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -251,18 +251,33 @@ func applyStateDelta(kv basics.TealKeyValue, stateDelta basics.StateDelta) error return nil } +// applyError is an error type that may be returned by applyEvalDelta in case +// the transaction execution should not fail for a clear state program. This is +// to distinguish failures due to schema violations from failures due to system +// faults (e.g. a failed database read). +type applyError struct { + msg string +} + +func (a *applyError) Error() string { + return a.msg +} + +func isApplyError(err error) bool { + _, ok := err.(*applyError) + return ok +} + // applyEvalDelta applies a basics.EvalDelta to the app's global key/value // store as well as a set of local key/value stores. If this function returns // an error, the transaction must not be committed. // -// The errIfNotApplied parameter is set to false when applying the results of a -// ClearState program. ClearState programs are not allowed to reject -// transactions for any reason. So if the ClearState program passes, -// but returns an invalid evalDelta that we cannot apply (e.g. because it would -// violate a schema), then errIfNotApplied = false instructs applyEvalDelta to -// return a nil error. For system errors (e.g. failing to fetch or write a -// balance record), applyEvalDelta will always return a non-nil error. -func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, params basics.AppParams, creator, sender basics.Address, balances Balances, appIdx basics.AppIndex, errIfNotApplied bool) error { +// If the delta we are applying was generated by a ClearStateProgram, then a +// failure to apply the delta does not necessarily mean the transaction should +// be rejected. For example, if the delta would exceed a state schema, then +// we don't want to apply the changes, but we also don't want to fail. In these +// situations, we return an applyError. +func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, params basics.AppParams, creator, sender basics.Address, balances Balances, appIdx basics.AppIndex) error { /* * 1. Apply GlobalState delta (if any), allocating the key/value store * if required. @@ -283,10 +298,7 @@ func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, p // key/value lengths err := evalDelta.GlobalDelta.Valid(&proto) if err != nil { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("cannot apply GlobalState delta: %v", err) + return &applyError{fmt.Sprintf("cannot apply GlobalState delta: %v", err)} } // Apply the GlobalDelta in place on the cloned copy @@ -298,10 +310,7 @@ func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, p // Make sure we haven't violated the GlobalStateSchema err = params.GlobalState.SatisfiesSchema(params.GlobalStateSchema) if err != nil { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("GlobalState for app %d would use too much space: %v", appIdx, err) + return &applyError{fmt.Sprintf("GlobalState for app %d would use too much space: %v", appIdx, err)} } } @@ -324,29 +333,20 @@ func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, p // invalid EvalDelta _, ok := changes[addr] if ok { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("duplicate LocalState delta for %s", addr.String()) + return &applyError{fmt.Sprintf("duplicate LocalState delta for %s", addr.String())} } // Zero-length LocalState deltas are not allowed. We should never produce // them from Eval. if len(delta) == 0 { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("got zero-length delta for %s, not allowed", addr.String()) + return &applyError{fmt.Sprintf("got zero-length delta for %s, not allowed", addr.String())} } // Check that the local state delta isn't breaking any rules regarding // key/value lengths err = delta.Valid(&proto) if err != nil { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("cannot apply LocalState delta for %s: %v", addr.String(), err) + return &applyError{fmt.Sprintf("cannot apply LocalState delta for %s: %v", addr.String(), err)} } record, err := balances.Get(addr, false) @@ -356,10 +356,7 @@ func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, p localState, ok := record.AppLocalStates[appIdx] if !ok { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("cannot apply LocalState delta to %s: acct has not opted in to app %d", addr.String(), appIdx) + return &applyError{fmt.Sprintf("cannot apply LocalState delta to %s: acct has not opted in to app %d", addr.String(), appIdx)} } // Clone LocalState so that we have a copy that is safe to modify @@ -379,10 +376,7 @@ func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, p // Make sure we haven't violated the LocalStateSchema err = localState.KeyValue.SatisfiesSchema(localState.Schema) if err != nil { - if !errIfNotApplied { - return nil - } - return fmt.Errorf("LocalState for %s for app %d would use too much space: %v", addr.String(), appIdx, err) + return &applyError{fmt.Sprintf("LocalState for %s for app %d would use too much space: %v", addr.String(), appIdx, err)} } // Stage the change to be committed after all schema checks @@ -565,16 +559,16 @@ func (ac *ApplicationCallTxnFields) applyClearState( // deltas. Apply them, provided they don't exceed the bounds set by // the GlobalStateSchema and LocalStateSchema. If they do exceed // those bounds, then don't fail, but also don't apply the changes. - failIfNotApplied := false - err = ac.applyEvalDelta(evalDelta, params, creator, sender, - balances, appIdx, failIfNotApplied) - if err != nil { + err = ac.applyEvalDelta(evalDelta, params, creator, sender, balances, appIdx) + if err != nil && !isApplyError(err) { return err } - // Fill in applyData, so that consumers don't have to implement a - // stateful TEAL interpreter to apply state changes - ad.EvalDelta = evalDelta + // If we applied the changes, fill in applyData, so that consumers don't + // have to implement a stateful TEAL interpreter to apply state changes + if err == nil { + ad.EvalDelta = evalDelta + } } else { // Ignore errors and rejections from the ClearStateProgram } @@ -724,9 +718,7 @@ func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec // Apply GlobalState and LocalState deltas, provided they don't exceed // the bounds set by the GlobalStateSchema and LocalStateSchema. // If they would exceed those bounds, then fail. - failIfNotApplied := true - err = ac.applyEvalDelta(evalDelta, params, creator, header.Sender, - balances, appIdx, failIfNotApplied) + err = ac.applyEvalDelta(evalDelta, params, creator, header.Sender, balances, appIdx) if err != nil { return err } diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 397938af08..9853897ca0 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -432,16 +432,14 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { var params basics.AppParams var appIdx basics.AppIndex var b testBalances - var errIfNotApplied = false // check empty input - err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(0, b.put) a.Equal(0, b.putWith) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(0, b.put) a.Equal(0, b.putWith) @@ -451,14 +449,13 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { // check global on unsupported proto b.SetProto(protocol.ConsensusV23) - errIfNotApplied = false - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) - a.NoError(err) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) a.Equal(0, b.put) a.Equal(0, b.putWith) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "cannot apply GlobalState delta") a.Equal(0, b.put) @@ -466,23 +463,21 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { // check global on supported proto b.SetProto(protocol.ConsensusFuture) - errIfNotApplied = false - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) - a.NoError(err) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) a.Equal(0, b.put) a.Equal(0, b.putWith) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space", appIdx)) a.Equal(0, b.put) a.Equal(0, b.putWith) // check Action=Delete delta on empty params - errIfNotApplied = false ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.DeleteAction} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "balance not found") a.Equal(0, b.put) @@ -495,7 +490,7 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { creator = getRandomAddress(a) b.balances = make(map[basics.Address]basics.AccountData) b.balances[creator] = basics.AccountData{} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(1, b.put) a.Equal(0, b.putWith) @@ -519,14 +514,14 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { // now check errors with non-default values b.SetProto(protocol.ConsensusV23) ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) - a.NoError(err) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) a.Equal(0, b.put) a.Equal(0, b.putWith) a.Equal(basics.AccountData{}, b.balances[creator]) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "cannot apply GlobalState delta") a.Equal(0, b.put) @@ -534,7 +529,7 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { a.Equal(basics.AccountData{}, b.balances[creator]) b.SetProto(protocol.ConsensusFuture) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store integer count", appIdx)) a.Equal(0, b.put) @@ -544,7 +539,7 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { // try illformed delta params.GlobalStateSchema = basics.StateSchema{NumUint: 1} ed.GlobalDelta["bytes"] = basics.ValueDelta{Action: basics.SetBytesAction, Uint: 1} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store bytes count", appIdx)) a.Equal(0, b.put) @@ -556,7 +551,7 @@ func TestAppCallApplyGlobalStateDeltas(t *testing.T) { br := 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 - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(1, b.put) pad, ok = b.putBalances[creator] @@ -583,18 +578,16 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { var params basics.AppParams var appIdx basics.AppIndex var b testBalances - var errIfNotApplied = false b.balances = make(map[basics.Address]basics.AccountData) ed.LocalDeltas = make(map[uint64]basics.StateDelta) - err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(0, b.put) a.Equal(0, b.putWith) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(0, b.put) a.Equal(0, b.putWith) @@ -602,17 +595,15 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { ed.LocalDeltas[1] = basics.StateDelta{} // non-existing account - errIfNotApplied = false - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) // empty delta ac.Accounts = append(ac.Accounts, sender, sender) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Equal(0, b.put) a.Equal(0, b.putWith) @@ -622,15 +613,14 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { ed.LocalDeltas[0] = basics.StateDelta{"uint": basics.ValueDelta{Action: basics.DeleteAction}} ed.LocalDeltas[1] = basics.StateDelta{"bytes": basics.ValueDelta{Action: basics.DeleteAction}} b.balances[sender] = basics.AccountData{} - errIfNotApplied = false - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) - a.NoError(err) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) a.Equal(0, b.put) a.Equal(0, b.putWith) a.Equal(basics.AccountData{}, b.balances[sender]) // not opted in - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "acct has not opted in to app") a.Equal(0, b.put) @@ -644,8 +634,7 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { b.balances[sender] = basics.AccountData{ AppLocalStates: cp, } - errIfNotApplied = true - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "duplicate LocalState delta") a.Equal(0, b.put) @@ -662,7 +651,7 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { "bytes": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "value"}, } delete(ed.LocalDeltas, 1) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.Error(err) a.Contains(err.Error(), "would use too much space") a.Equal(0, b.put) @@ -677,7 +666,7 @@ func TestAppCallApplyLocalsStateDeltas(t *testing.T) { Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, }} b.balances[sender] = basics.AccountData{AppLocalStates: cp} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx, errIfNotApplied) + err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) a.NoError(err) a.Equal(1, b.put) a.Equal(0, b.putWith) @@ -1012,6 +1001,7 @@ func TestAppCallClearState(t *testing.T) { ad := &ApplyData{} b.appCreators = make(map[basics.AppIndex]basics.Address) b.balances = make(map[basics.Address]basics.AccountData, 2) + b.SetProto(protocol.ConsensusFuture) // check app not exist and not opted in b.balances[sender] = basics.AccountData{} @@ -1058,11 +1048,17 @@ func TestAppCallClearState(t *testing.T) { // check existing application with failing ClearStateProgram b.balances[creator] = basics.AccountData{ AppParams: map[basics.AppIndex]basics.AppParams{ - appIdx: {ClearStateProgram: []byte{1}}, + appIdx: { + ClearStateProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + }, }, } b.appCreators[appIdx] = creator + // one put: to opt out steva.pass = false gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} steva.delta = basics.EvalDelta{GlobalDelta: gd} @@ -1077,11 +1073,12 @@ func TestAppCallClearState(t *testing.T) { b.ResetWrites() - // check existing application with successful ClearStateProgram + // check existing application with successful ClearStateProgram. two + // puts: one to write global state, one to opt out steva.pass = true err = ac.applyClearState(&b, sender, appIdx, ad, &steva) a.NoError(err) - a.Equal(1, b.put) + a.Equal(2, b.put) a.Equal(0, b.putWith) a.Equal(0, len(br.AppLocalStates)) a.Equal(basics.StateSchema{}, br.TotalAppSchema) From b031432fd96d869fba6400eb814ef7eb0ce3398d Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 15:28:02 -0400 Subject: [PATCH 127/267] Add few comments. --- ledger/accountdb.go | 3 +++ ledger/acctupdates.go | 37 +++++++++++++++++++++++++++++++++++++ ledger/acctupdates_test.go | 5 +++++ 3 files changed, 45 insertions(+) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 5ccb77caf6..8ebfffaa04 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -97,6 +97,9 @@ var accountsResetExprs = []string{ `DROP TABLE IF EXISTS accounthashes`, } +// accountDBVersion is the database version that this binary would know how to support and how to upgrade to. +// details about the content of each of the versions can be found in the upgrade functions upgradeDatabaseSchemaXXXX +// and their descriptions. var accountDBVersion = int32(2) type accountDelta struct { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 6ecb56e393..24b77f900d 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -777,6 +777,8 @@ func accountHashBuilder(addr basics.Address, accountData basics.AccountData, enc } // Initialize accounts DB if needed and return account round +// as part of the initialization, it tests the current database schema version, and perform upgrade +// procedures to bring it up to the database schema supported by the binary. func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (basics.Round, error) { // check current database version. dbVersion, err := db.GetUserVersion(ctx, tx) @@ -800,11 +802,13 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b case 0: dbVersion, err = au.upgradeDatabaseSchema0(ctx, tx) if err != nil { + au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 0 : %v", err) return 0, err } case 1: dbVersion, err = au.upgradeDatabaseSchema1(ctx, tx) if err != nil { + au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 1 : %v", err) return 0, err } default: @@ -898,6 +902,25 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b } // upgradeDatabaseSchema0 upgrades the database schema from version 0 to version 1 +// +// schema of version 0 is expected to be aligned with the schema used on version 2.0.8 or before. +// any database of version 2.0.8 would be of version 0. The schema of the database might be empty +// ( in case we've just created the database ) or it migth include the following tables: +// * acctrounds +// * accounttotals +// * accountbase +// * assetcreators +// * storedcatchpoints +// * accounthashes +// * catchpointstate +// +// As the first step of the upgrade, the above tables are being created if they do not already exists. +// Following that, the assetcreators tables is being altered and a new column named ctype is being added. +// Last, the database was just created, it would initialize it with the following: +// The accountbase would get initialized with the au.initAccounts +// The accounttotals would get initialized to align with the initialization account added to accountbase +// The acctrounds would get updated to indicate that the balance matches round 0 +// func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { au.log.Infof("accountsInitialize initializing schema") err = accountsInit(tx, au.initAccounts, au.initProto) @@ -912,6 +935,20 @@ func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx } // upgradeDatabaseSchema1 upgrades the database schema from version 1 to version 2 +// +// The schema updated to verison 2 intended to ensure that the encoding of all the accounts data is +// both canonical and identical across the entire network. On release 2.0.5 we released an upgrade to the messagepack. +// the upgraded messagepack was decoding the message into the same datastructures, but would have different +// encoding compared to it's predecessor. As a result, some of the account data were stored in inconsistent way ( across differrent nodes ). +// To address this, this startup proceduce would attempt to scan all the accounts data. for each account data, we would +// see if it's encoding aligned with the current messagepack encoder. If it doesn't we would update it's encoding. +// than, depending if we found any such account data, we would reset the merkle trie and stored catchpoints. +// once the upgrade is complete, the accountsInitialize would (if needed) rebuild the merke trie using the new +// encoded accounts. +// +// This upgrade doesn't change any of the actual database schema ( i.e. tables, indexes ) but rather just performing +// a functional update to it's content. +// func (au *accountUpdates) upgradeDatabaseSchema1(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { // update accounts encoding. au.log.Infof("accountsInitialize verifying accounts data encoding") diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 2653f2f1c3..6df9ae0e67 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -786,6 +786,11 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { t.Run("DiskDB", testFunction) } +// TestAcctUpdatesDeleteStoredCatchpoints - The goal of this test is to verify that the deleteStoredCatchpoints function works correctly. +// it doing so by filling up the storedcatchpoints with dummy catchpoint file entries, as well as creating these dummy files on disk. +// ( the term dummy is only because these aren't real catchpoint files, but rather a zero-length file ). Then, the test call the function +// and ensures that it did not errored, the catchpoint files were correctly deleted, and that deleteStoredCatchpoints contains no more +// entries. func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] From bbc3c77647e4a0a2a50c4526c365d50e7654514d Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 15:38:53 -0400 Subject: [PATCH 128/267] update. --- ledger/acctupdates.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 24b77f900d..6ceba82e90 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -903,9 +903,9 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b // upgradeDatabaseSchema0 upgrades the database schema from version 0 to version 1 // -// schema of version 0 is expected to be aligned with the schema used on version 2.0.8 or before. -// any database of version 2.0.8 would be of version 0. The schema of the database might be empty -// ( in case we've just created the database ) or it migth include the following tables: +// Schema of version 0 is expected to be aligned with the schema used on version 2.0.8 or before. +// Any database of version 2.0.8 would be of version 0. At this point, the database might +// have the following tables : ( i.e. a newly created table would not have these ) // * acctrounds // * accounttotals // * accountbase @@ -915,8 +915,8 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b // * catchpointstate // // As the first step of the upgrade, the above tables are being created if they do not already exists. -// Following that, the assetcreators tables is being altered and a new column named ctype is being added. -// Last, the database was just created, it would initialize it with the following: +// Following that, the assetcreators table is being altered by adding a new column to it (ctype). +// Last, in case the database was just created, it would get initialized it with the following: // The accountbase would get initialized with the au.initAccounts // The accounttotals would get initialized to align with the initialization account added to accountbase // The acctrounds would get updated to indicate that the balance matches round 0 @@ -938,10 +938,11 @@ func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx // // The schema updated to verison 2 intended to ensure that the encoding of all the accounts data is // both canonical and identical across the entire network. On release 2.0.5 we released an upgrade to the messagepack. -// the upgraded messagepack was decoding the message into the same datastructures, but would have different -// encoding compared to it's predecessor. As a result, some of the account data were stored in inconsistent way ( across differrent nodes ). +// the upgraded messagepack was decoding the account data correctly, but would have different +// encoding compared to it's predecessor. As a result, some of the account data that was previously stored +// would have different encoded representation than the one on disk. // To address this, this startup proceduce would attempt to scan all the accounts data. for each account data, we would -// see if it's encoding aligned with the current messagepack encoder. If it doesn't we would update it's encoding. +// see if it's encoding aligns with the current messagepack encoder. If it doesn't we would update it's encoding. // than, depending if we found any such account data, we would reset the merkle trie and stored catchpoints. // once the upgrade is complete, the accountsInitialize would (if needed) rebuild the merke trie using the new // encoded accounts. From 87d0ca51e341941553b03c41fe93f06be9265654 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 21 Jul 2020 16:42:10 -0400 Subject: [PATCH 129/267] rename acctupdt internal methods to be XXXImpl --- ledger/acctupdates.go | 34 ++++++++++++++++++---------------- ledger/acctupdates_test.go | 16 ++++++++-------- ledger/ledger.go | 22 ++++++++++++++-------- ledger/ledger_test.go | 4 ++++ 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 1e6e7a9d0a..3b88ac327a 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -267,18 +267,20 @@ func (au *accountUpdates) close() { func (au *accountUpdates) Lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - return au.lookup(rnd, addr, withRewards) + return au.lookupImpl(rnd, addr, withRewards) } -// listAssets lists the assets by their asset index, limiring to the first maxResults -func (au *accountUpdates) listAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.CreatableLocator, error) { +// ListAssets lists the assets by their asset index, limiting to the first maxResults +func (au *accountUpdates) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) ([]basics.CreatableLocator, error) { return au.listCreatables(basics.CreatableIndex(maxAssetIdx), maxResults, basics.AssetCreatable) } -func (au *accountUpdates) listApplications(maxAppIdx basics.AppIndex, maxResults uint64) ([]basics.CreatableLocator, error) { +// ListApplications lists the application by their app index, limiting to the first maxResults +func (au *accountUpdates) ListApplications(maxAppIdx basics.AppIndex, maxResults uint64) ([]basics.CreatableLocator, error) { return au.listCreatables(basics.CreatableIndex(maxAppIdx), maxResults, basics.AppCreatable) } +// listCreatables lists the application/asset by their app/asset index, limiting to the first maxResults func (au *accountUpdates) listCreatables(maxCreatableIdx basics.CreatableIndex, maxResults uint64, ctype basics.CreatableType) ([]basics.CreatableLocator, error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() @@ -353,7 +355,7 @@ func (au *accountUpdates) listCreatables(maxCreatableIdx basics.CreatableIndex, } // getLastCatchpointLabel retrieves the last catchpoint label that was stored to the database. -func (au *accountUpdates) getLastCatchpointLabel() string { +func (au *accountUpdates) GetLastCatchpointLabel() string { au.accountsMu.RLock() defer au.accountsMu.RUnlock() return au.lastCatchpointLabel @@ -363,7 +365,7 @@ func (au *accountUpdates) getLastCatchpointLabel() string { func (au *accountUpdates) GetCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - return au.getCreatorForRound(rnd, cidx, ctype) + return au.getCreatorForRoundImpl(rnd, cidx, ctype) } // committedUpTo enqueues commiting the balances for round committedRound-lookback. @@ -487,10 +489,10 @@ func (au *accountUpdates) newBlock(blk bookkeeping.Block, delta StateDelta) { func (au *accountUpdates) Totals(rnd basics.Round) (totals AccountTotals, err error) { au.accountsMu.RLock() defer au.accountsMu.RUnlock() - return au.totals(rnd) + return au.totalsImpl(rnd) } -func (au *accountUpdates) getCatchpointStream(round basics.Round) (io.ReadCloser, error) { +func (au *accountUpdates) GetCatchpointStream(round basics.Round) (io.ReadCloser, error) { dbFileName := "" err := au.dbs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { dbFileName, _, _, err = getCatchpoint(tx, round) @@ -576,12 +578,12 @@ func (aul *accountUpdatesLedgerEvaluator) BlockHdr(r basics.Round) (bookkeeping. // Lookup returns the account balance for a given address at a given round func (aul *accountUpdatesLedgerEvaluator) Lookup(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { - return aul.au.lookup(rnd, addr, true) + return aul.au.lookupImpl(rnd, addr, true) } // Totals returns the totals for a given round func (aul *accountUpdatesLedgerEvaluator) Totals(rnd basics.Round) (AccountTotals, error) { - return aul.au.totals(rnd) + return aul.au.totalsImpl(rnd) } func (aul *accountUpdatesLedgerEvaluator) isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) { @@ -597,15 +599,15 @@ func (aul *accountUpdatesLedgerEvaluator) GetRoundTxIds(rnd basics.Round) (txMap // Lookup returns the account balance for a given address at a given round, without the reward func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { - return aul.au.lookup(rnd, addr, false) + return aul.au.lookupImpl(rnd, addr, false) } // GetCreatorForRound returns the asset/app creator for a given asset/app index at a given round func (aul *accountUpdatesLedgerEvaluator) GetCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { - return aul.au.getCreatorForRound(rnd, cidx, ctype) + return aul.au.getCreatorForRoundImpl(rnd, cidx, ctype) } -func (au *accountUpdates) totals(rnd basics.Round) (totals AccountTotals, err error) { +func (au *accountUpdates) totalsImpl(rnd basics.Round) (totals AccountTotals, err error) { offset, err := au.roundOffset(rnd) if err != nil { return @@ -1094,7 +1096,7 @@ func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) au.roundTotals = append(au.roundTotals, newTotals) } -func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { +func (au *accountUpdates) lookupImpl(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { offset, err := au.roundOffset(rnd) if err != nil { return @@ -1137,8 +1139,8 @@ func (au *accountUpdates) lookup(rnd basics.Round, addr basics.Address, withRewa return au.accountsq.lookup(addr) } -// getCreatorForRound returns the asset/app creator for a given asset/app index at a given round -func (au *accountUpdates) getCreatorForRound(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { +// getCreatorForRoundImpl returns the asset/app creator for a given asset/app index at a given round +func (au *accountUpdates) getCreatorForRoundImpl(rnd basics.Round, cidx basics.CreatableIndex, ctype basics.CreatableType) (creator basics.Address, ok bool, err error) { offset, err := au.roundOffset(rnd) if err != nil { return basics.Address{}, false, err diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 03323a588f..4cc049db23 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -147,14 +147,14 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, lates _, err := au.Totals(latest + 1) require.Error(t, err) - _, err = au.lookup(latest+1, randomAddress(), false) + _, err = au.Lookup(latest+1, randomAddress(), false) require.Error(t, err) if base > 0 { _, err := au.Totals(base - 1) require.Error(t, err) - _, err = au.lookup(base-1, randomAddress(), false) + _, err = au.Lookup(base-1, randomAddress(), false) require.Error(t, err) } @@ -180,7 +180,7 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, lates var totalOnline, totalOffline, totalNotPart uint64 for addr, data := range accts[rnd] { - d, err := au.lookup(rnd, addr, false) + d, err := au.Lookup(rnd, addr, false) require.NoError(t, err) require.Equal(t, d, data) @@ -211,7 +211,7 @@ func checkAcctUpdates(t *testing.T, au *accountUpdates, base basics.Round, lates require.Equal(t, totals.Participating().Raw, totalOnline+totalOffline) require.Equal(t, totals.All().Raw, totalOnline+totalOffline+totalNotPart) - d, err := au.lookup(rnd, randomAddress(), false) + d, err := au.Lookup(rnd, randomAddress(), false) require.NoError(t, err) require.Equal(t, d, basics.AccountData{}) } @@ -697,14 +697,14 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { updates := make(map[basics.Address]accountDelta) moneyAccountsExpectedAmounts = append(moneyAccountsExpectedAmounts, make([]uint64, len(moneyAccounts))) toAccount := moneyAccounts[0] - toAccountDataOld, err := au.lookup(i-1, toAccount, false) + toAccountDataOld, err := au.Lookup(i-1, toAccount, false) require.NoError(t, err) toAccountDataNew := toAccountDataOld for j := 1; j < len(moneyAccounts); j++ { fromAccount := moneyAccounts[j] - fromAccountDataOld, err := au.lookup(i-1, fromAccount, false) + fromAccountDataOld, err := au.Lookup(i-1, fromAccount, false) require.NoError(t, err) require.Equalf(t, moneyAccountsExpectedAmounts[i-1][j], fromAccountDataOld.MicroAlgos.Raw, "Account index : %d\nRound number : %d", j, i) @@ -733,7 +733,7 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { if checkRound < uint64(testback) { continue } - acct, err := au.lookup(basics.Round(checkRound-uint64(testback)), moneyAccounts[j], false) + acct, err := au.Lookup(basics.Round(checkRound-uint64(testback)), moneyAccounts[j], false) // we might get an error like "round 2 before dbRound 5", which is the success case, so we'll ignore it. if err != nil { // verify it's the expected error and not anything else. @@ -777,7 +777,7 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { au.waitAccountsWriting() for idx, addr := range moneyAccounts { - balance, err := au.lookup(lastRound, addr, false) + balance, err := au.Lookup(lastRound, addr, false) require.NoErrorf(t, err, "unable to retrieve balance for account idx %d %v", idx, addr) if idx != 0 { require.Equalf(t, 100*1000000-roundCount*(roundCount-1)/2, int(balance.MicroAlgos.Raw), "account idx %d %v has the wrong balance", idx, addr) diff --git a/ledger/ledger.go b/ledger/ledger.go index 2fa5de8f06..1906ce150f 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -138,20 +138,21 @@ func OpenLedger( func (l *Ledger) reloadLedger() error { // similar to the Close function, we want to start by closing the blockQ first. The - // blockQ is having a sync goroutine which indirectly calls other trackers. We want to eliminate the path first, + // blockQ is having a sync goroutine which indirectly calls other trackers. We want to eliminate that go-routine first, // and follow up by taking the trackers lock. if l.blockQ != nil { l.blockQ.close() l.blockQ = nil } + // take the trackers lock. This would ensure that no other goroutine is using the trackers. l.trackerMu.Lock() defer l.trackerMu.Unlock() - // close the trackers. At this point, we already have the trackers write lock which ensures that noone is current using these. + // close the trackers. l.trackers.close() - // reload. + // reload - var err error l.blockQ, err = bqInit(l) if err != nil { @@ -168,7 +169,7 @@ func (l *Ledger) reloadLedger() error { err = l.trackers.loadFromDisk(l) if err != nil { - err = fmt.Errorf("reloadLedger.reloadLedger %v", err) + err = fmt.Errorf("reloadLedger.loadFromDisk %v", err) return err } @@ -286,6 +287,11 @@ func (l *Ledger) Close() { l.blockQ.close() l.blockQ = nil } + + // take the trackers lock. This would ensure that no other goroutine is using the trackers. + l.trackerMu.Lock() + defer l.trackerMu.Unlock() + // then, we shut down the trackers and their corresponding goroutines. l.trackers.close() @@ -321,7 +327,7 @@ func (l *Ledger) notifyCommit(r basics.Round) basics.Round { func (l *Ledger) GetLastCatchpointLabel() string { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getLastCatchpointLabel() + return l.accts.GetLastCatchpointLabel() } // GetCreatorForRound takes a CreatableIndex and a CreatableType and tries to @@ -347,7 +353,7 @@ func (l *Ledger) GetCreator(cidx basics.CreatableIndex, ctype basics.CreatableTy func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.listAssets(maxAssetIdx, maxResults) + return l.accts.ListAssets(maxAssetIdx, maxResults) } // ListApplications takes a maximum app index and maximum result length, and @@ -356,7 +362,7 @@ func (l *Ledger) ListAssets(maxAssetIdx basics.AssetIndex, maxResults uint64) (r func (l *Ledger) ListApplications(maxAppIdx basics.AppIndex, maxResults uint64) (results []basics.CreatableLocator, err error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.listApplications(maxAppIdx, maxResults) + return l.accts.ListApplications(maxAppIdx, maxResults) } // Lookup uses the accounts tracker to return the account state for a @@ -532,7 +538,7 @@ func (l *Ledger) GetCatchpointCatchupState(ctx context.Context) (state Catchpoin func (l *Ledger) GetCatchpointStream(round basics.Round) (io.ReadCloser, error) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() - return l.accts.getCatchpointStream(round) + return l.accts.GetCatchpointStream(round) } // ledgerForTracker methods diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index ca9ad2835e..7ca813bdc5 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -907,6 +907,10 @@ func TestLedgerReload(t *testing.T) { if i%7 == 0 { l.reloadLedger() + // if we reloaded it before it got committed, we need to roll back the round counter. + if l.LatestCommitted() != blk.BlockHeader.Round { + blk.BlockHeader.Round = l.LatestCommitted() + } } if i%13 == 0 { l.WaitForCommit(blk.Round()) From 81da4ac69a9beb9305851dbb26053fde9dc79bb2 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 22 Jul 2020 12:14:20 -0400 Subject: [PATCH 130/267] Add comments around the accounts database upgrade procedures (#1278) Per past review comments (76ac4f6), I'm adding some more in-code comments. --- ledger/accountdb.go | 3 +++ ledger/acctupdates.go | 38 ++++++++++++++++++++++++++++++++++++++ ledger/acctupdates_test.go | 5 +++++ 3 files changed, 46 insertions(+) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 5ccb77caf6..8ebfffaa04 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -97,6 +97,9 @@ var accountsResetExprs = []string{ `DROP TABLE IF EXISTS accounthashes`, } +// accountDBVersion is the database version that this binary would know how to support and how to upgrade to. +// details about the content of each of the versions can be found in the upgrade functions upgradeDatabaseSchemaXXXX +// and their descriptions. var accountDBVersion = int32(2) type accountDelta struct { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 6ecb56e393..f0be58c71e 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -777,6 +777,8 @@ func accountHashBuilder(addr basics.Address, accountData basics.AccountData, enc } // Initialize accounts DB if needed and return account round +// as part of the initialization, it tests the current database schema version, and perform upgrade +// procedures to bring it up to the database schema supported by the binary. func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (basics.Round, error) { // check current database version. dbVersion, err := db.GetUserVersion(ctx, tx) @@ -800,11 +802,13 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b case 0: dbVersion, err = au.upgradeDatabaseSchema0(ctx, tx) if err != nil { + au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 0 : %v", err) return 0, err } case 1: dbVersion, err = au.upgradeDatabaseSchema1(ctx, tx) if err != nil { + au.log.Warnf("accountsInitialize failed to upgrade accounts database (ledger.tracker.sqlite) from schema 1 : %v", err) return 0, err } default: @@ -898,6 +902,25 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b } // upgradeDatabaseSchema0 upgrades the database schema from version 0 to version 1 +// +// Schema of version 0 is expected to be aligned with the schema used on version 2.0.8 or before. +// Any database of version 2.0.8 would be of version 0. At this point, the database might +// have the following tables : ( i.e. a newly created database would not have these ) +// * acctrounds +// * accounttotals +// * accountbase +// * assetcreators +// * storedcatchpoints +// * accounthashes +// * catchpointstate +// +// As the first step of the upgrade, the above tables are being created if they do not already exists. +// Following that, the assetcreators table is being altered by adding a new column to it (ctype). +// Last, in case the database was just created, it would get initialized with the following: +// The accountbase would get initialized with the au.initAccounts +// The accounttotals would get initialized to align with the initialization account added to accountbase +// The acctrounds would get updated to indicate that the balance matches round 0 +// func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { au.log.Infof("accountsInitialize initializing schema") err = accountsInit(tx, au.initAccounts, au.initProto) @@ -912,6 +935,21 @@ func (au *accountUpdates) upgradeDatabaseSchema0(ctx context.Context, tx *sql.Tx } // upgradeDatabaseSchema1 upgrades the database schema from version 1 to version 2 +// +// The schema updated to verison 2 intended to ensure that the encoding of all the accounts data is +// both canonical and identical across the entire network. On release 2.0.5 we released an upgrade to the messagepack. +// the upgraded messagepack was decoding the account data correctly, but would have different +// encoding compared to it's predecessor. As a result, some of the account data that was previously stored +// would have different encoded representation than the one on disk. +// To address this, this startup proceduce would attempt to scan all the accounts data. for each account data, we would +// see if it's encoding aligns with the current messagepack encoder. If it doesn't we would update it's encoding. +// then, depending if we found any such account data, we would reset the merkle trie and stored catchpoints. +// once the upgrade is complete, the accountsInitialize would (if needed) rebuild the merke trie using the new +// encoded accounts. +// +// This upgrade doesn't change any of the actual database schema ( i.e. tables, indexes ) but rather just performing +// a functional update to it's content. +// func (au *accountUpdates) upgradeDatabaseSchema1(ctx context.Context, tx *sql.Tx) (updatedDBVersion int32, err error) { // update accounts encoding. au.log.Infof("accountsInitialize verifying accounts data encoding") diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 2653f2f1c3..6df9ae0e67 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -786,6 +786,11 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { t.Run("DiskDB", testFunction) } +// TestAcctUpdatesDeleteStoredCatchpoints - The goal of this test is to verify that the deleteStoredCatchpoints function works correctly. +// it doing so by filling up the storedcatchpoints with dummy catchpoint file entries, as well as creating these dummy files on disk. +// ( the term dummy is only because these aren't real catchpoint files, but rather a zero-length file ). Then, the test call the function +// and ensures that it did not errored, the catchpoint files were correctly deleted, and that deleteStoredCatchpoints contains no more +// entries. func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { proto := config.Consensus[protocol.ConsensusCurrentVersion] From c3d1d33740d061ca67ccb6d6149f2f5e3444a625 Mon Sep 17 00:00:00 2001 From: egieseke Date: Wed, 22 Jul 2020 13:24:19 -0400 Subject: [PATCH 131/267] Eric/create stateful teal expect test (#1261) Added expect test for stateful teal: create, optin, call, delete. Added README.md file. --- test/e2e-go/cli/goal/expect/README.md | 55 +++++++ .../cli/goal/expect/goalExpectCommon.exp | 128 ++++++++++++++-- .../goal/expect/statefulTealCreateAppTest.exp | 144 ++++++++++++++++++ .../tealprogs/clear_program_state.teal | 4 + 4 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 test/e2e-go/cli/goal/expect/README.md create mode 100644 test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp create mode 100644 test/scripts/e2e_subs/tealprogs/clear_program_state.teal diff --git a/test/e2e-go/cli/goal/expect/README.md b/test/e2e-go/cli/goal/expect/README.md new file mode 100644 index 0000000000..1f7194640a --- /dev/null +++ b/test/e2e-go/cli/goal/expect/README.md @@ -0,0 +1,55 @@ +# Goal Testing with Expect + +Expect is a framework for testing command line interfaces (CLI). It is a extension to the TCL shell designed to automate invoking, interacting with, and validating results of CLIs. + +We use expect to test the Algorand Goal CLI. + +## Setup + +From the go-algorand root directory, setup the environment and build the binaries as described in the top-level project README.md file. + +#### Initialize the project +```bash +git clone https://github.com/algorand/go-algorand +cd go-algorand +./scripts/configure_dev.sh +``` +#### Build the binaries +```bash +make clean install +``` + +#### Running the integration tests + +Running the integration tests will invoke the expect tests. Execute the following command to run the integration tests. + +```bash +make integration +``` + +#### Set environment variables + +The `GOPATH` should be set to your local Go projects directory. +The `PATH` environment variable should include `$GOPATH/bin`. For example: + +```bash +export GOPATH=~/path/to/goprojects +export PATH=$(go env GOPATH | cut -d':' -f1 ):${PATH} +``` + + +## Running the Expect Tests + +To run the Goal Expect test, run the following command from the top level go-algorand directory: + +`go test -v test/e2e-go/cli/goal/expect/goal_expect_test.go` + + +## Adding New Tests + +To add a test, create a copy of the `test/e2e-go/cli/goal/expect/basicGoalTest.exp` file within the same directory. +Give it a name that reflects the purpose of the test, and make sure the the file name suffix matches `'Test.exp'`. This will allow it to be included when running the expect tests. + +## Common Procedures + +Reusable and commonly used goal commands can be defined as procedures. This helps reduce code bulk and errors in the expect tests. See the file `goalExpectCommon.exp` for the list of available procedures. diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index f720d817bd..51f07f9288 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -69,13 +69,13 @@ proc ::AlgorandGoal::StartNode { TEST_ALGO_DIR {SYSTEMD_MANAGED "False"} {PEER_A spawn goal {*}$GOAL_PARAMS if { $SYSTEMD_MANAGED eq "True" } { expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node start" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node start" } "^This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} eof { ::AlgorandGoal::CheckEOF "Unable to start network" } } } else { expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node start" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node start" } "^Algorand node successfully started!*" {puts "Node started successfully"; close} eof { ::AlgorandGoal::CheckEOF "Unable to start network" } } @@ -96,16 +96,16 @@ proc ::AlgorandGoal::StopNode { TEST_ALGO_DIR {SYSTEMD_MANAGED ""} } { if { $SYSTEMD_MANAGED eq "" } { spawn goal node stop -d $TEST_ALGO_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node stop" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node stop" } "*The node was successfully stopped.*" {puts "Node stopped successfully"; close} - eof { close; ::AlgorandGoal::Abort "Did not recieve appropriate message before goal command completion" } + eof { close; ::AlgorandGoal::Abort "Did not receive appropriate message before goal command completion" } } } else { spawn goal node stop -d $TEST_ALGO_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node stop" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node stop" } "*This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} - eof { close; ::AlgorandGoal::Abort "Did not recieve appropriate message before goal command completion" } + eof { close; ::AlgorandGoal::Abort "Did not receive appropriate message before goal command completion" } } } } EXCEPTION] } { @@ -124,13 +124,13 @@ proc ::AlgorandGoal::RestartNode { TEST_ALGO_DIR {SYSTEMD_MANAGED ""} } { if { $SYSTEMD_MANAGED eq "" } { spawn goal node restart -d $TEST_ALGO_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node restart" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node restart" } "^The node was successfully stopped.*Algorand node successfully started!*" {puts "Node restarted successfully"; close} } } else { spawn goal node restart -d $TEST_ALGO_DIR expect { - timeout { close; ::AlgorandGoal::Abort "Did not recieve appropriate message during node restart" } + timeout { close; ::AlgorandGoal::Abort "Did not receive appropriate message during node restart" } "^This node is managed by systemd, you must run the following command to make your desired state change to your node:*" { puts "Goal showed correct error message for systemd" ; close} } } @@ -381,7 +381,7 @@ proc ::AlgorandGoal::AccountTransfer { FROM_WALLET_NAME FROM_WALLET_PASSWORD FRO set timeout 60 if { [ catch { set TRANSACTION_ID "NOT SET" - spawn goal clerk send --fee $FEE_AMOUNT --wallet $FROM_WALLET_NAME --amount $TRANSFER_AMOUNT --from $FROM_ACCOUNT_ADDRESS --to $TO_ACCOUNT_ADDRESS -d $TEST_PRIMARY_NODE_DIR -N + spawn goal clerk send --fee $FEE_AMOUNT --wallet $FROM_WALLET_NAME --amount $TRANSFER_AMOUNT --from $FROM_ACCOUNT_ADDRESS --to $TO_ACCOUNT_ADDRESS -d $TEST_PRIMARY_NODE_DIR expect { timeout { close; ::AlgorandGoal::Abort "Timed out transferring funds" } "Please enter the password for wallet '$FROM_WALLET_NAME':" { send "$FROM_WALLET_PASSWORD\r" } @@ -896,6 +896,116 @@ proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRI } } +# stateful teal app procedures + + +# App Create +proc ::AlgorandGoal::AppCreate { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APPROVAL_PROGRAM APP_ARG GLOBAL_BYTE_SLICES LOCAL_BYTE_SLICES CLEAR_PROGRAM DATA_DIR } { + set timeout 60 + if { [ catch { + set APP_ID "NOT SET" + puts "calling goal app create" + spawn goal app create --creator $ACCOUNT_ADDRESS --approval-prog $APPROVAL_PROGRAM --global-byteslices $GLOBAL_BYTE_SLICES --global-ints 0 --local-byteslices $LOCAL_BYTE_SLICES --local-ints 0 --app-arg $APP_ARG --clear-prog $CLEAR_PROGRAM -w $WALLET_NAME -d $DATA_DIR + expect { + timeout { abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue } + -re {Created app with app index ([0-9]+)} {set APP_ID $expect_out(1,string) ;close } + eof {close; ::AlgorandGoal::Abort "app not created" } + } + puts "App ID $APP_ID" + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCreate: $EXCEPTION" + } + return $APP_ID +} + + +# App OptIn with 1 argument +proc ::AlgorandGoal::AppOptIn { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 DATA_DIR } { + set timeout 60 + if { [ catch { + puts "opting in account to app" + spawn goal app optin --app-id $APP_ID --app-arg $APP_ARG1 -w $WALLET_NAME --from $ACCOUNT_ADDRESS -d $DATA_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "optin successful"; close} + eof {close; ::AlgorandGoal::Abort "optin failed" } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppOptIn: $EXCEPTION" + } +} + +# App Call with 1 argument +proc ::AlgorandGoal::AppCall { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 DATA_DIR } { + set timeout 60 + if { [ catch { + puts "app call with 1 arg: '$APP_ARG1'" + spawn goal app call --app-id $APP_ID --app-arg $APP_ARG1 -w $WALLET_NAME --from $ACCOUNT_ADDRESS -d $DATA_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "app call succeeded"; close} + eof {close; ::AlgorandGoal::Abort "app call failed" } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCall: $EXCEPTION" + } +} + +# App Call with 2 arguments +proc ::AlgorandGoal::AppCall2 { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 APP_ARG2 DATA_DIR } { + set timeout 60 + if { [ catch { + puts "app call with 2 args: '$APP_ARG1' and '$APP_ARG2'" + spawn goal app call --app-id $APP_ID --app-arg $APP_ARG1 --app-arg $APP_ARG2 -w $WALLET_NAME --from $ACCOUNT_ADDRESS -d $DATA_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "app call succeeded"; close} + eof {close; ::AlgorandGoal::Abort "app call failed" } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCall2: $EXCEPTION" + } +} + +# App Closeout with 1 argument +proc ::AlgorandGoal::AppCloseout { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 DATA_DIR } { + set timeout 60 + if { [ catch { + puts "calling goal app close out with first arg '$APP_ARG1'" + spawn goal app closeout --app-id $APP_ID --app-arg $APP_ARG1 -w $WALLET_NAME --from $ACCOUNT_ADDRESS -d $DATA_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue } + "*committed in round*" {puts "app closeout succeeded"; close} + eof {close; ::AlgorandGoal::Abort "app closeout failed"} + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCloseout: $EXCEPTION" + } +} + +# App Delete with 1 argument +proc ::AlgorandGoal::AppDelete { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 DATA_DIR } { + set timeout 60 + if { [ catch { + puts "call app delete with '$APP_ARG1'" + spawn goal app delete --app-id $APP_ID --app-arg $APP_ARG1 -w $WALLET_NAME --from $ACCOUNT_ADDRESS -d $DATA_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "app delete successful"; close} + eof {close; ::AlgorandGoal::Abort "app delete failed" } + } + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppDelete: $EXCEPTION" + } +} + + # CheckEOF checks if there was an error, and aborts if there was an error proc ::AlgorandGoal::CheckEOF { { ERROR_STRING "" } } { upvar spawn_id spawn_id diff --git a/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp b/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp new file mode 100644 index 0000000000..3a40f0241b --- /dev/null +++ b/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp @@ -0,0 +1,144 @@ +#!/usr/bin/expect -f +#exp_internal 1 +set err 0 +log_user 1 + +source goalExpectCommon.exp + + +set TEST_ALGO_DIR [lindex $argv 0] +set TEST_DATA_DIR [lindex $argv 1] + +proc statefulTealTest { TEST_ALGO_DIR TEST_DATA_DIR TEAL_PROGRAM} { + + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] + puts "Primary Node Address: $PRIMARY_NODE_ADDRESS" + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + # Determine primary account + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Check the balance of the primary account + set PRIMARY_ACCOUNT_BALANCE [::AlgorandGoal::GetAccountBalance $PRIMARY_WALLET_NAME $PRIMARY_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR] + puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" + + ::AlgorandGoal::WaitForRound 1 $TEST_PRIMARY_NODE_DIR + + # Create wallet #1 + set WALLET_1_NAME Wallet_1_$TIME_STAMP + set WALLET_1_PASSWORD 1234 + set WALLET_1_PASSPHRASE [::AlgorandGoal::CreateWallet $WALLET_1_NAME $WALLET_1_PASSWORD $TEST_PRIMARY_NODE_DIR] + puts "WALLET_1_PASSPHRASE: $WALLET_1_PASSPHRASE" + ::AlgorandGoal::VerifyWallet $WALLET_1_NAME $TEST_PRIMARY_NODE_DIR + + # Associate a new account with the wallet + set ACCOUNT_1_ADDRESS [::AlgorandGoal::CreateAccountForWallet $WALLET_1_NAME $WALLET_1_PASSWORD $TEST_PRIMARY_NODE_DIR] + ::AlgorandGoal::VerifyAccount $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS $TEST_PRIMARY_NODE_DIR + + # Transfer Algos from primary account to account 1 + set MIN_BALANCE 1000000 + set TRANSFER_AMOUNT [expr {1000 * $MIN_BALANCE}] + set FEE_AMOUNT 1000 + set TRANSACTION_ID [::AlgorandGoal::AccountTransfer $PRIMARY_WALLET_NAME "" $PRIMARY_ACCOUNT_ADDRESS $TRANSFER_AMOUNT $ACCOUNT_1_ADDRESS $FEE_AMOUNT $TEST_PRIMARY_NODE_DIR] + + set TEAL_PROGS_DIR "$TEST_DATA_DIR/../scripts/e2e_subs/tealprogs" + + if { $TEAL_PROGRAM == "loccheck.teal" } { + set GLOBAL_BYTE_SLICES 0 + set LOCAL_BYTE_SLICES 1 + } else { + set GLOBAL_BYTE_SLICES 1 + set LOCAL_BYTE_SLICES 0 + } + + set APP_ID [::AlgorandGoal::AppCreate $PRIMARY_WALLET_NAME "" $PRIMARY_ACCOUNT_ADDRESS ${TEAL_PROGS_DIR}/${TEAL_PROGRAM} "str:hello" $GLOBAL_BYTE_SLICES $LOCAL_BYTE_SLICES ${TEAL_PROGS_DIR}/clear_program_state.teal $TEST_PRIMARY_NODE_DIR] + + # Application call with no args should fail + puts "calling goal app call with no args" + spawn goal app call --app-id $APP_ID --from $PRIMARY_ACCOUNT_ADDRESS -w $PRIMARY_WALLET_NAME -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Couldn't broadcast tx with algod: HTTP 400 Bad Request: TransactionPool.Remember: transaction*" {puts "received expected error"; close} + eof { close; ::AlgorandGoal::Abort "did not receive expected error" } + } + + if { $TEAL_PROGRAM == "loccheck.teal" } { + # Application call with arg0 == "write" should fail before we opt in + puts "calling goal app call with account that has not opted in" + spawn goal app call --app-id $APP_ID --app-arg "str:write" -w $WALLET_1_NAME --from $ACCOUNT_1_ADDRESS -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_1_NAME':" {send "$WALLET_1_PASSWORD\r" ; exp_continue } + "*not opted in to app*" {puts "received expected error"; close} + "*exceeds schema bytes count*" {puts "received expected error"; close} + "Couldn't broadcast tx with algod: HTTP 400 Bad Request: TransactionPool.Remember: transaction*" {puts "received expected error"; close} + eof { close; ::AlgorandGoal::Abort "did not receive expected error" } + } + } + + # Optin account to app + ::AlgorandGoal::AppOptIn $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:hello" $TEST_PRIMARY_NODE_DIR + + # Application call with arg0 == "write" should succeed after opt in + ::AlgorandGoal::AppCall $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:write" $TEST_PRIMARY_NODE_DIR + + # Check should now succeed with value "bar" + ::AlgorandGoal::AppCall2 $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:check" "str:bar" $TEST_PRIMARY_NODE_DIR + + # Should succeed to close out with first arg hello + ::AlgorandGoal::AppCloseout $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:hello" $TEST_PRIMARY_NODE_DIR + + # Should succeed to opt in with first arg write + ::AlgorandGoal::AppOptIn $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:write" $TEST_PRIMARY_NODE_DIR + + # Check should still succeed + ::AlgorandGoal::AppCall2 $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:check" "str:bar" $TEST_PRIMARY_NODE_DIR + + # Delete application should still succeed + ::AlgorandGoal::AppDelete $APP_ID $WALLET_1_NAME $WALLET_1_PASSWORD $ACCOUNT_1_ADDRESS "str:hello" $TEST_PRIMARY_NODE_DIR + + # Shutdown the network + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + + puts "Goal Stateful Teal test Successful" +} + + +if { [catch { + source goalExpectCommon.exp + + puts "starting test" + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + puts "calling statefulTealTest with loccheck.teal" + + statefulTealTest $TEST_ALGO_DIR $TEST_DATA_DIR loccheck.teal + + puts "calling statefulTealTest with globcheck.teal" + + statefulTealTest $TEST_ALGO_DIR $TEST_DATA_DIR globcheck.teal + + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in statefulTealCreateAppTest: $EXCEPTION" +} diff --git a/test/scripts/e2e_subs/tealprogs/clear_program_state.teal b/test/scripts/e2e_subs/tealprogs/clear_program_state.teal new file mode 100644 index 0000000000..f83715e4b5 --- /dev/null +++ b/test/scripts/e2e_subs/tealprogs/clear_program_state.teal @@ -0,0 +1,4 @@ +#pragma version 2 +// This program clears program state + +int 1 From 25c1634dba0abfac5f2c6f139e760c3a7acce114 Mon Sep 17 00:00:00 2001 From: egieseke Date: Wed, 22 Jul 2020 13:25:52 -0400 Subject: [PATCH 132/267] Fix linux arm64 nightly build deploy (#1274) Fix Linux Arm64 deploy on Travis. --- scripts/travis/deploy_packages.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/travis/deploy_packages.sh b/scripts/travis/deploy_packages.sh index 927fc8c91f..94e12d340f 100755 --- a/scripts/travis/deploy_packages.sh +++ b/scripts/travis/deploy_packages.sh @@ -9,6 +9,20 @@ # Examples: scripts/travis/deploy_packages.sh set -e + +SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" + +# Use go version specified by get_golang_version.sh +if ! GOLANG_VERSION=$("${SCRIPTPATH}/../get_golang_version.sh") +then + echo "${GOLANG_VERSION}" + exit 1 +fi + +curl -sL -o ~/gimme https://raw.githubusercontent.com/travis-ci/gimme/master/gimme +chmod +x ~/gimme +eval $(~/gimme "${GOLANG_VERSION}") + scripts/travis/build.sh export RELEASE_GENESIS_PROCESS=true From 3a2f71a9e010bc053969f42dfcf2a6c15d502f48 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 22 Jul 2020 15:19:49 -0400 Subject: [PATCH 133/267] Remove unused GetRoundTxIds from ledgerForEvaluator interface (#1285) The function GetRoundTxIds is no longer used by the evaluator, and not needed to be part of the ledgerForEvaluator interface. The backing implementation on Ledger was left as is for future usage. --- agreement/voteTracker.go | 2 +- crypto/merkletrie/node.go | 2 +- ledger/acctupdates.go | 2 +- ledger/catchpointwriter.go | 2 +- ledger/eval.go | 1 - ledger/ledger.go | 1 + scripts/travis/external_build_printlog.sh | 2 +- test/e2e-go/cli/goal/expect/goalExpectCommon.exp | 2 +- util/metrics/serviceCommon.go | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) diff --git a/agreement/voteTracker.go b/agreement/voteTracker.go index 9d27feb14b..fd420a435a 100644 --- a/agreement/voteTracker.go +++ b/agreement/voteTracker.go @@ -220,7 +220,7 @@ func (tracker *voteTracker) handle(r routerHandle, p player, e0 event) event { // at this point, we need to check if this is the very last vote or not. // if we have no regular votes, we won't be generating a bundle so we can abort right here. - // note that it migth be a legit thing; if we received two votes from X followed by 100 regular votes, + // note that it might be a legit thing; if we received two votes from X followed by 100 regular votes, // we would end up here for the second vote. if len(tracker.Voters) == 0 { return res diff --git a/crypto/merkletrie/node.go b/crypto/merkletrie/node.go index 51f1c69ff0..0c96821a87 100644 --- a/crypto/merkletrie/node.go +++ b/crypto/merkletrie/node.go @@ -272,7 +272,7 @@ func (n *node) remove(cache *merkleTrieCache, key []byte, path []byte) (nodeID s } cache.deleteNode(childNodeID) - // at this point, we migth end up with a single leaf child. collapse that. + // at this point, we might end up with a single leaf child. collapse that. if pnode.childrenNext[pnode.firstChild] == pnode.firstChild { childNode, err = cache.getNode(pnode.children[pnode.firstChild]) if err != nil { diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index f0be58c71e..4b1d355164 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -847,7 +847,7 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b return 0, fmt.Errorf("accountsInitialize was unable to MakeTrie: %v", err) } - // we migth have a database that was previously initialized, and now we're adding the balances trie. In that case, we need to add all the existing balances to this trie. + // we might have a database that was previously initialized, and now we're adding the balances trie. In that case, we need to add all the existing balances to this trie. // we can figure this out by examinine the hash of the root: rootHash, err := trie.RootHash() if err != nil { diff --git a/ledger/catchpointwriter.go b/ledger/catchpointwriter.go index 8fbd531853..9756e2340e 100644 --- a/ledger/catchpointwriter.go +++ b/ledger/catchpointwriter.go @@ -46,7 +46,7 @@ const ( // catchpointWriter is the struct managing the persistance of accounts data into the catchpoint file. // it's designed to work in a step fashion : a caller will call the WriteStep method in a loop until -// the writing is complete. It migth take multiple steps until the operation is over, and the caller +// the writing is complete. It might take multiple steps until the operation is over, and the caller // has the option of throtteling the CPU utilization in between the calls. type catchpointWriter struct { hasher hash.Hash diff --git a/ledger/eval.go b/ledger/eval.go index 0c649a4e57..ae637fd661 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -185,7 +185,6 @@ type ledgerForEvaluator interface { Lookup(basics.Round, basics.Address) (basics.AccountData, error) Totals(basics.Round) (AccountTotals, error) isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) - GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) LookupWithoutRewards(basics.Round, basics.Address) (basics.AccountData, error) GetCreatorForRound(basics.Round, basics.CreatableIndex, basics.CreatableType) (basics.Address, bool, error) } diff --git a/ledger/ledger.go b/ledger/ledger.go index 9a4d0b8b68..8a1d47b7f8 100644 --- a/ledger/ledger.go +++ b/ledger/ledger.go @@ -396,6 +396,7 @@ func (l *Ledger) isDup(currentProto config.ConsensusParams, current basics.Round } // GetRoundTxIds returns a map of the transactions ids that we have for the given round +// this function is currently not being used, but remains here as it migth be useful in the future. func (l *Ledger) GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) { l.trackerMu.RLock() defer l.trackerMu.RUnlock() diff --git a/scripts/travis/external_build_printlog.sh b/scripts/travis/external_build_printlog.sh index 54d7a8665e..8bd5f6f767 100755 --- a/scripts/travis/external_build_printlog.sh +++ b/scripts/travis/external_build_printlog.sh @@ -59,7 +59,7 @@ while [ $SECONDS -lt $end ]; do if [ "${TRAVIS}" = "true" ]; then # under travis, if we have passed the 1h:45m mark, the build is going to likely fail due to timeout. # instead of failing, we want to exit the build with success, indicating where the log could be retrieved later on. - # ( note that there migth be an issue with that build, but we don't want to cap it via travis timeouts. ) + # ( note that there might be an issue with that build, but we don't want to cap it via travis timeouts. ) if [ $((SECONDS-LOG_WAITING_START)) -gt $((60*105)) ]; then echo "Build is taking too long. Travis is going to timeout this build, so we'll tell travis that we're done for now." echo "Once this build is complete, you could get a complete log by typing:" diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index 51f07f9288..4b4b2522e5 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -882,7 +882,7 @@ proc ::AlgorandGoal::AddParticipationKey { ADDRESS FIRST_ROUND LAST_ROUND TEST_P # Register online participation with a given account proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRIMARY_NODE_DIR } { - # we need to set the timeout to more than one round, since it migth take few rounds for the transaction to be accepted, transmitted, proposed, etc. + # we need to set the timeout to more than one round, since it might take few rounds for the transaction to be accepted, transmitted, proposed, etc. set timeout 20 if { [ catch { diff --git a/util/metrics/serviceCommon.go b/util/metrics/serviceCommon.go index 54647fdd04..8cc240226d 100644 --- a/util/metrics/serviceCommon.go +++ b/util/metrics/serviceCommon.go @@ -24,7 +24,7 @@ import ( ) // ServiceConfig would contain all the information we need in order to create a listening server endpoint. -// We migth want to support rolling port numbers so that we could easily support multiple endpoints per machine. +// We might want to support rolling port numbers so that we could easily support multiple endpoints per machine. // ( note that multiple endpoints per machine doesn't solve the question "how would the prometheus server figure that out") type ServiceConfig struct { NodeExporterListenAddress string From 9770534c1814346e6cad0c13788dbaf5ccc8c4e5 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Wed, 22 Jul 2020 16:40:12 -0400 Subject: [PATCH 134/267] -- Add the ability to convert a regular transaction to a multisig with signer field for rekeying. --- cmd/algokey/multisig.go | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/cmd/algokey/multisig.go b/cmd/algokey/multisig.go index d1a254176e..ade690fe1b 100644 --- a/cmd/algokey/multisig.go +++ b/cmd/algokey/multisig.go @@ -18,9 +18,12 @@ package main import ( "fmt" + "github.com/algorand/go-algorand/data/basics" "io" "io/ioutil" "os" + "strconv" + "strings" "github.com/spf13/cobra" @@ -34,13 +37,22 @@ var multisigTxfile string var multisigOutfile string var multisigMnemonic string +var msigParams string + func init() { + + multisigCmd.AddCommand(convertCmd) + multisigCmd.Flags().StringVarP(&multisigKeyfile, "keyfile", "k", "", "Private key filename") multisigCmd.Flags().StringVarP(&multisigMnemonic, "mnemonic", "m", "", "Private key mnemonic") multisigCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") multisigCmd.MarkFlagRequired("txfile") multisigCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename") multisigCmd.MarkFlagRequired("outfile") + + convertCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig params - Threshold PK1 PK2 ...") + convertCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") + } var multisigCmd = &cobra.Command{ @@ -93,3 +105,78 @@ var multisigCmd = &cobra.Command{ } }, } + +var convertCmd = &cobra.Command{ + Use: "convert -t [transaction file] -p \"[threshold] [PK1] [PK2] ...\"", + Short: "Adds the necessary fields to a transaction that is sent from an account to was rekeied to a multisig account.", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, _ []string) { + + // Read Transaction + txdata, err := ioutil.ReadFile(multisigTxfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot read transactions from %s: %v\n", multisigTxfile, err) + os.Exit(1) + } + + var outBytes []byte + dec := protocol.NewDecoderBytes(txdata) + + var stxn transactions.SignedTxn + err = dec.Decode(&stxn) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot decode transaction: %v\n", err) + os.Exit(1) + } + + // Decode params + params := strings.Split(msigParams, " ") + if len(params) < 2 { + _, _ = fmt.Fprint(os.Stderr, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and addresses") + os.Exit(1) + } + + threshold, err := strconv.ParseUint(params[0], 10, 8) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse the threshold. Make sure it's a number between 1 and 255: %v\n", err) + os.Exit(1) + } + + // convert addresses to pks + // convert the addresses into public keys + pks := make([]crypto.PublicKey, len(params[1:])) + for i, addrStr := range params[1:] { + addr, err := basics.UnmarshalChecksumAddress(addrStr) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot decode address: %v\n", err) + os.Exit(1) + } + pks[i] = crypto.PublicKey(addr) + } + + addr, err := crypto.MultisigAddrGen(1, uint8(threshold), pks) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot generate multisig addr: %v\n", err) + os.Exit(1) + } + + //gen the multisig and assign to the txn + stxn.Msig = crypto.MultisigPreimageFromPKs(1, uint8(threshold), pks) + + //append the signer since it's a rekey txn + if basics.Address(addr) == stxn.Txn.Sender { + fmt.Fprintf(os.Stderr, "The sender at the msig address should not be the same: %v\n", err) + os.Exit(1) + } + stxn.AuthAddr = basics.Address(addr) + + // Write the txn + outBytes = append(outBytes, protocol.Encode(&stxn)...) + + err = ioutil.WriteFile(multisigTxfile, outBytes, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "Cannot write transactions to %s: %v\n", multisigOutfile, err) + os.Exit(1) + } + }, +} From 6064051bdec298c4eff845f8212a3000a5351328 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 22 Jul 2020 19:15:51 -0400 Subject: [PATCH 135/267] TEAL: txna assembly and disassembly fixes (#1282) txn with two args is assembler-level alias for txna, and this requires some additional checks 1. Fix copy-paste error in txna disassemble routine 2. Fix assembling 'txn Accounts' without second argument --- data/transactions/logic/assembler.go | 12 ++- data/transactions/logic/assembler_test.go | 84 ++++++++++++++++++- .../transactions/logic/backwardCompat_test.go | 27 ++++-- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 86e8faec3e..9dca82bf63 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -707,6 +707,10 @@ func assembleTxn(ops *OpStream, spec *OpSpec, args []string) error { if !ok { return fmt.Errorf("txn unknown arg %s", args[0]) } + _, ok = txnaFieldSpecByField[fs.field] + if ok { + return fmt.Errorf("found txna field %s in txn op", args[0]) + } if fs.version > ops.Version { return fmt.Errorf("txn %s available in version %d. Missed #pragma version?", args[0], fs.version) } @@ -714,7 +718,7 @@ func assembleTxn(ops *OpStream, spec *OpSpec, args []string) error { return ops.Txn(uint64(val)) } -// assembleTxn2 generates txn or txna opcode depending on number of operands +// assembleTxn2 delegates to assembleTxn or assembleTxna depending on number of operands func assembleTxn2(ops *OpStream, spec *OpSpec, args []string) error { if len(args) == 1 { return assembleTxn(ops, spec, args) @@ -760,6 +764,10 @@ func assembleGtxn(ops *OpStream, spec *OpSpec, args []string) error { if !ok { return fmt.Errorf("gtxn unknown arg %s", args[1]) } + _, ok = txnaFieldSpecByField[fs.field] + if ok { + return fmt.Errorf("found gtxna field %s in gtxn op", args[1]) + } if fs.version > ops.Version { return fmt.Errorf("gtxn %s available in version %d. Missed #pragma version?", args[1], fs.version) } @@ -1541,7 +1549,7 @@ func disTxna(dis *disassembleState, spec *OpSpec) { return } arrayFieldIdx := dis.program[dis.pc+2] - _, dis.err = fmt.Fprintf(dis.out, "txn %s %d\n", TxnFieldNames[txarg], arrayFieldIdx) + _, dis.err = fmt.Fprintf(dis.out, "txna %s %d\n", TxnFieldNames[txarg], arrayFieldIdx) } func disGtxn(dis *disassembleState, spec *OpSpec) { diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index d056c1af67..1610443194 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -363,6 +363,34 @@ func TestAssembleTxna(t *testing.T) { _, err = AssembleStringV2(source) require.Error(t, err) require.Contains(t, err.Error(), "strconv.ParseUint") + + source = `txn Accounts` + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) + require.Error(t, err) + require.Contains(t, err.Error(), "found txna field Accounts in txn op") + + source = `txn Accounts` + _, err = AssembleStringV1(source) + require.Error(t, err) + require.Contains(t, err.Error(), "found txna field Accounts in txn op") + + source = `txn Accounts 0` + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) + require.NoError(t, err) + + source = `gtxn 0 Accounts` + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) + require.Error(t, err) + require.Contains(t, err.Error(), "found gtxna field Accounts in gtxn op") + + source = `gtxn 0 Accounts` + _, err = AssembleStringV1(source) + require.Error(t, err) + require.Contains(t, err.Error(), "found gtxna field Accounts in gtxn op") + + source = `gtxn 0 Accounts 1` + _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) + require.NoError(t, err) } func TestAssembleGlobal(t *testing.T) { @@ -835,9 +863,9 @@ txn GroupIndex txn TxID txn ApplicationID txn OnCompletion -txn ApplicationArgs +txna ApplicationArgs 0 txn NumAppArgs -txn Accounts +txna Accounts 0 txn NumAccounts txn ApprovalProgram txn ClearStateProgram @@ -1061,7 +1089,7 @@ func TestAssembleAsset(t *testing.T) { } func TestDisassembleSingleOp(t *testing.T) { - // test ensures no double arg_0 entries in disassebly listing + // test ensures no double arg_0 entries in disassembly listing sample := "// version 2\narg_0\n" program, err := AssembleStringWithVersion(sample, AssemblerMaxVersion) require.NoError(t, err) @@ -1071,6 +1099,56 @@ func TestDisassembleSingleOp(t *testing.T) { require.Equal(t, sample, disassembled) } +func TestDisassembleTxna(t *testing.T) { + // check txn and txna are properly disassembled + txnSample := "// version 2\ntxn Sender\n" + program, err := AssembleStringWithVersion(txnSample, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err := Disassemble(program) + require.NoError(t, err) + require.Equal(t, txnSample, disassembled) + + txnaSample := "// version 2\ntxna Accounts 0\n" + program, err = AssembleStringWithVersion(txnaSample, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err = Disassemble(program) + require.NoError(t, err) + require.Equal(t, txnaSample, disassembled) + + txnSample2 := "// version 2\ntxn Accounts 0\n" + program, err = AssembleStringWithVersion(txnSample2, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err = Disassemble(program) + require.NoError(t, err) + // comapre with txnaSample, not txnSample2 + require.Equal(t, txnaSample, disassembled) +} + +func TestDisassembleGtxna(t *testing.T) { + // check gtxn and gtxna are properly disassembled + gtxnSample := "// version 2\ngtxn 0 Sender\n" + program, err := AssembleStringWithVersion(gtxnSample, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err := Disassemble(program) + require.NoError(t, err) + require.Equal(t, gtxnSample, disassembled) + + gtxnaSample := "// version 2\ngtxna 0 Accounts 0\n" + program, err = AssembleStringWithVersion(gtxnaSample, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err = Disassemble(program) + require.NoError(t, err) + require.Equal(t, gtxnaSample, disassembled) + + gtxnSample2 := "// version 2\ngtxn 0 Accounts 0\n" + program, err = AssembleStringWithVersion(gtxnSample2, AssemblerMaxVersion) + require.NoError(t, err) + disassembled, err = Disassemble(program) + require.NoError(t, err) + // comapre with gtxnaSample, not gtxnSample2 + require.Equal(t, gtxnaSample, disassembled) +} + func TestAssembleOffsets(t *testing.T) { source := "err" program, offsets, err := AssembleStringWithVersionEx(source, AssemblerMaxVersion) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index 1c6fb3af07..daa8242e22 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -395,10 +395,10 @@ func TestBackwardCompatGlobalFields(t *testing.T) { // ensure v2 fields error in v1 program func TestBackwardCompatTxnFields(t *testing.T) { - var fields []string + var fields []txnFieldSpec for _, fs := range txnFieldSpecs { if fs.version > 1 { - fields = append(fields, fs.field.String()) + fields = append(fields, fs) } } require.Greater(t, len(fields), 1) @@ -411,27 +411,40 @@ func TestBackwardCompatTxnFields(t *testing.T) { ledger := makeTestLedger(nil) txn := makeSampleTxn() txgroup := makeSampleTxnGroup(txn) - for _, field := range fields { + for _, fs := range fields { + field := fs.field.String() for _, command := range tests { text := fmt.Sprintf(command, field) + asmError := "available in version 2" + if _, ok := txnaFieldSpecByField[fs.field]; ok { + parts := strings.Split(text, " ") + op := parts[0] + asmError = fmt.Sprintf("found %sa field %s in %s op", op, field, op) + } // check V1 assembler fails program, err := AssembleStringWithVersion(text, 0) require.Error(t, err) - require.Contains(t, err.Error(), "available in version 2") + require.Contains(t, err.Error(), asmError) require.Nil(t, program) program, err = AssembleStringWithVersion(text, 1) require.Error(t, err) - require.Contains(t, err.Error(), "available in version 2") + require.Contains(t, err.Error(), asmError) require.Nil(t, program) program, err = AssembleString(text) require.Error(t, err) - require.Contains(t, err.Error(), "available in version 2") + require.Contains(t, err.Error(), asmError) require.Nil(t, program) program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) - require.NoError(t, err) + if _, ok := txnaFieldSpecByField[fs.field]; ok { + // "txn Accounts" is invalid, so skip evaluation + require.Error(t, err, asmError) + continue + } else { + require.NoError(t, err) + } proto := config.Consensus[protocol.ConsensusV23] ep := defaultEvalParams(nil, nil) From 6363c2b18635071408a14d5edfa189665d03034b Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Wed, 22 Jul 2020 19:16:29 -0400 Subject: [PATCH 136/267] Tealdbg: use round number and latestTimestamp from dryrun-req if available (#1281) While documenting TEAL testing in Python, I found tealdbg does not use Round and LatestTimestamp from dryrun request object that creates difficulties with tests debugging. --- cmd/tealdbg/local.go | 8 ++++++++ cmd/tealdbg/localLedger.go | 4 ++-- cmd/tealdbg/main.go | 10 +++++----- cmd/tealdbg/server.go | 4 ++-- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index bf22c7417b..240e7dd533 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -291,6 +291,14 @@ func (r *LocalRunner) Setup(dp *DebugParams) (err error) { balances[record.Addr] = record.AccountData } + if dp.Round == 0 && ddr.Round != 0 { + dp.Round = ddr.Round + } + + if dp.LatestTimestamp == 0 && ddr.LatestTimestamp != 0 { + dp.LatestTimestamp = int64(ddr.LatestTimestamp) + } + // if program(s) specified then run from it if len(dp.ProgramBlobs) > 0 { if len(r.txnGroup) == 1 && dp.GroupIndex != 0 { diff --git a/cmd/tealdbg/localLedger.go b/cmd/tealdbg/localLedger.go index e49d009213..ff7103bc68 100644 --- a/cmd/tealdbg/localLedger.go +++ b/cmd/tealdbg/localLedger.go @@ -33,12 +33,12 @@ type balancesAdapter struct { txnGroup []transactions.SignedTxn groupIndex int proto config.ConsensusParams - round int + round uint64 } func makeAppLedger( balances map[basics.Address]basics.AccountData, txnGroup []transactions.SignedTxn, - groupIndex int, proto config.ConsensusParams, round int, latestTimestamp int64, + groupIndex int, proto config.ConsensusParams, round uint64, latestTimestamp int64, appIdx basics.AppIndex, painless bool, ) (logic.LedgerForLogic, appState, error) { diff --git a/cmd/tealdbg/main.go b/cmd/tealdbg/main.go index f06f5978a5..b43cf96b1d 100644 --- a/cmd/tealdbg/main.go +++ b/cmd/tealdbg/main.go @@ -129,7 +129,7 @@ var txnFile string var groupIndex int var balanceFile string var ddrFile string -var roundNumber int +var roundNumber uint64 var timestamp int64 var runMode runModeValue = runModeValue{makeCobraStringValue("auto", []string{"signature", "application"})} var port int @@ -138,7 +138,7 @@ var noBrowserCheck bool var noSourceMap bool var verbose bool var painless bool -var appID int +var appID uint64 func init() { rootCmd.PersistentFlags().VarP(&frontend, "frontend", "f", "Frontend to use: "+frontend.AllowedString()) @@ -155,8 +155,8 @@ func init() { debugCmd.Flags().IntVarP(&groupIndex, "group-index", "g", 0, "Transaction index in a txn group") debugCmd.Flags().StringVarP(&balanceFile, "balance", "b", "", "Balance records to evaluate stateful TEAL on in form of json or msgpack file") debugCmd.Flags().StringVarP(&ddrFile, "dryrun-req", "d", "", "Program(s) and state(s) in dryrun REST request format") - debugCmd.Flags().IntVarP(&appID, "app-id", "a", 1380011588, "Application ID for stateful TEAL if not set in transaction(s)") - debugCmd.Flags().IntVarP(&roundNumber, "round", "r", 1095518031, "Ledger round number to evaluate stateful TEAL on") + debugCmd.Flags().Uint64VarP(&appID, "app-id", "a", 1380011588, "Application ID for stateful TEAL if not set in transaction(s)") + debugCmd.Flags().Uint64VarP(&roundNumber, "round", "r", 0, "Ledger round number to evaluate stateful TEAL on") debugCmd.Flags().Int64VarP(×tamp, "latest-timestamp", "l", 0, "Latest confirmed timestamp to evaluate stateful TEAL on") debugCmd.Flags().VarP(&runMode, "mode", "m", "TEAL evaluation mode: "+runMode.AllowedString()) debugCmd.Flags().BoolVar(&painless, "painless", false, "Automatically create balance record for all accounts and applications") @@ -248,7 +248,7 @@ func debugLocal(args []string) { GroupIndex: groupIndex, BalanceBlob: balanceBlob, DdrBlob: ddrBlob, - Round: roundNumber, + Round: uint64(roundNumber), LatestTimestamp: timestamp, RunMode: runMode.String(), DisableSourceMap: noSourceMap, diff --git a/cmd/tealdbg/server.go b/cmd/tealdbg/server.go index cc2049eb18..723060b9f3 100644 --- a/cmd/tealdbg/server.go +++ b/cmd/tealdbg/server.go @@ -64,11 +64,11 @@ type DebugParams struct { GroupIndex int BalanceBlob []byte DdrBlob []byte - Round int + Round uint64 LatestTimestamp int64 RunMode string DisableSourceMap bool - AppID int + AppID uint64 Painless bool } From a26651a40bc1bfbd5dbf073d74fca1ca8fd8c13b Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 22 Jul 2020 19:17:05 -0400 Subject: [PATCH 137/267] Add additional type information to teal programs in spec. (#1277) Add this additional type information to the account model to keep it consistent with indexer. --- daemon/algod/api/algod.oas2.json | 6 +- daemon/algod/api/algod.oas3.yml | 6 +- .../api/server/v2/generated/private/routes.go | 248 +++++++------- .../algod/api/server/v2/generated/routes.go | 307 +++++++++--------- 4 files changed, 286 insertions(+), 281 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 5c23d8102e..3e2981b9bc 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1481,12 +1481,14 @@ "approval-program": { "description": "\\[approv\\] approval program.", "type": "string", - "format": "byte" + "format": "byte", + "x-algorand-format": "TEALProgram" }, "clear-state-program": { "description": "\\[clearp\\] approval program.", "type": "string", - "format": "byte" + "format": "byte", + "x-algorand-format": "TEALProgram" }, "local-state-schema": { "description": "[\\lsch\\] local schema", diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index f9d0460a47..2bdc68d9c8 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -824,13 +824,15 @@ "description": "\\[approv\\] approval program.", "format": "byte", "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", - "type": "string" + "type": "string", + "x-algorand-format": "TEALProgram" }, "clear-state-program": { "description": "\\[clearp\\] approval program.", "format": "byte", "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", - "type": "string" + "type": "string", + "x-algorand-format": "TEALProgram" }, "creator": { "description": "The address that created this application. This is the address where the parameters and global state for this application can be found.", diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 644c447f85..78e3b2720b 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -235,130 +235,130 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+x9f3PcNrLgV8HNe1WxfcMZ+Vd2rarUnmLlhy6O47KUvbu1fBsM2TODiAQYAtRo4tN3", - "v+oGQIIkODOytd5Nvf3L1gBoNBrdje5Go/lhkqqiVBKk0ZPjD5OSV7wAAxX9xdNU1dIkIsO/MtBpJUoj", - "lJwc+zamTSXkajKdCPy15GY9mU4kL6Dtg+Onkwp+q0UF2eTYVDVMJzpdQ8ERsNmW2LuBdJOsVOJAnFgQ", - "Z6eT2x0NPMsq0HqI5U8y3zIh07zOgJmKS81TbNJsI8yambXQzA1mQjIlgaklM+tOZ7YUkGd65hf5Ww3V", - "Nlilm3x8Sbctikmlchji+VIVCyHBYwUNUs2GMKNYBkvqtOaG4QyIq+9oFNPAq3TNlqrag6pFIsQXZF1M", - "jt9NNMgMKtqtFMQ1/XdZAfwOieHVCszk/TS2uKWBKjGiiCztzFG/Al3nRjPqS2tciWuQDEfN2I+1NmwB", - "jEv29tuX7OnTpy9wIQU3BjLHZKOramcP12SHT44nGTfgm4e8xvOVqrjMkqb/229f0vznboGH9uJaQ1xY", - "TrCFnZ2OLcAPjLCQkAZWtA8d7scREaFof17AUlVw4J7Yzve6KeH8/9RdSblJ16US0kT2hVErs81RHRYM", - "36XDGgQ6/UukVIVA3x0lL95/eDx9fHT7H+9Okr+5P58/vT1w+S8buHsoEO2Y1lUFMt0mqwo4ScuayyE9", - "3jp+0GtV5xlb82vafF6QqndjGY61qvOa5zXyiUgrdZKvlGbcsVEGS17nhvmJWS1zVFMIzXE7E5qVlboW", - "GWRT1L6btUjXLOXagqB+bCPyHHmw1pCN8Vp8dTuE6TYkCeL1UfSgBf3rEqNd1x5KwA1pgyTNlYbEqD3H", - "kz9xuMxYeKC0Z5W+22HFLtbAaHJssIct0U4iT+f5lhna14xxzTjzR9OUiSXbqpptaHNycUXj3WqQagVD", - "otHmdM5RFN4x8g2IESHeQqkcuCTiebkbkkwuxaquQLPNGszanXkV6FJJDUwtfoXU4Lb/z/OfXjNVsR9B", - "a76CNzy9YiBTlY3vsZs0doL/qhVueKFXJU+v4sd1LgoRQflHfiOKumCyLhZQ4X7588EoVoGpKzmGkIW4", - "h88KfjOc9KKqZUqb207bMdSQlYQuc76dsbMlK/jNV0dTh45mPM9ZCTITcsXMjRw10nDu/egllapldoAN", - "Y3DDglNTl5CKpYCMNVB2YOKm2YePkHfDp7WsAnQ8kFF0mln2oCPhJsIzKLrYwkq+goBlZuxnp7mo1agr", - "kI2CY4stNZUVXAtV62bQCI409W7zWioDSVnBUkR47NyRA7WH7ePUa+EMnFRJw4WEDDUvIa0MWE00ilMw", - "4W5nZnhEL7iGL5+NHeBt64G7v1T9Xd+54wftNnVKrEhGzkVsdQIbN5s64w9w/sK5tVgl9ufBRorVBR4l", - "S5HTMfMr7p8nQ61JCXQI4Q8eLVaSm7qC40v5CP9iCTs3XGa8yvCXwv70Y50bcS5W+FNuf3qlViI9F6sR", - "Yja4Rr0pGlbYfxBeXB2bm6jT8Eqpq7oMF5R2vNLFlp2djm2yhXlXxjxpXNnQq7i48Z7GXUeYm2YjR5Ac", - "pV3JseMVbCtAbHm6pH9ulsRPfFn9HiMmcq47YSka4KIEb91v+BPKOlhngJdlLlKO1JzTuXn8IcDkPytY", - "To4n/zFvQyRz26rnDq6dsbttD6AozfYhLv+khX//GLQjY1gEzUxIu13UdWqdxPvHB6FGMSHLtYfD17lK", - "rz4Kh7JSJVRG2P1dIJyh6BB4tgaeQcUybvis9bKs4TUiADTwexpHbhNUkTPvJ/oPzxk2o1hy4+05tGWF", - "RqtOBZGnDE1Ae7DYmbADmaaKFdbqY2it3QnLl+3kVmM3KvadI8v7PrTI7nxjDU1GI/wicOmtG3myUNXH", - "8UuPESRrnWPGEWpjDuPKuztLXesycfSJGNi2Qw9QG48c6tmQQn3wh9AqkOyWOueG/wOooxHqfVCnC+hz", - "UUcVpcjhHuR7zfV6uDi0kJ4+Yeffnzx//OTvT55/iUd8WalVxQu22BrQ7IE7mJg22xweDldMB0Wdmzj0", - "L595F6wLdy/lCOEG9iF0uwDUJJZizAYcELvTalvV8h5ICFWlqojRTCxlVKry5BoqLVQk/vHG9WCuB+ot", - "a7j3frfYsg3XDOcmf66WGVSzGOXRUSObwECh9x0sFvTFjWxp4wDyquLbwQ7Y9UZW5+Y9ZE+6xPfugWYl", - "VIm5kSyDRb0KzzS2rFTBOMtoICnQ1yqDc8NNre9BO7TAWmRwI0IU+ELVhnEmVYaCjp3jemMkGEpRGAoe", - "mVAVmbU9rxaA5nXK69XaMLRLVWxr24EJT+2mJHS26BHfsXH6bS87nQ205RXwbMsWAJKphXPQnOtIi+QU", - "1zH+ysZprRatxqno4FVWKgWtIUvc/dRe1PxdF22y2UEmwpvwbSZhWrElrz4SV6MMz/fgSX2G2OrW+nBO", - "7RDrw6bftX/9ycNd5BX6qJYJ0NRB4c7BwBgJ99KkLkfuM9xpdyEKFAkmuVQaUiUzHQWWc22SfaKAnTpH", - "Mm5rwH0x7ifAI177K66N9ZuFzMhssyJM89AYmmIc4VEtjZD/6hX0EHaKukfqWjfaWtdlqSoDWWwNEm52", - "zPUabpq51DKA3RwJRrFawz7IY1QK4Dti2ZVYAnHjAjdNYGm4OIqRo27dRknZQaIlxC5Ezn2vgLphTHcE", - "EbTxm5HEOEL3OKcJJE8n2qiyRJ1kklo248bIdG57n5if275D5uKm1ZWZApzdeJwc5htLWRvNX3O0lwgy", - "K/gV6nuyfqyDP8QZhTHRQqaQ7OJ8FMtz7BWKwB4hHTFI3X1hMFtPOHr8G2W6USbYswtjC76jdfzGhqsv", - "2lDOPRgIp2C4yHVjBDQx8XYWCp/3UxvQYqsgBWnyLfLwUlSFvYGis0P736yJkblZ7F1LK5YyYxVseJX5", - "HkOPxV10yQxu4vqWuzhBBjdMxBFdNrMJw1J/J+Qu0Wbxc4OucSxyOnbBRw3Ij4VIK8XtvR0S3p5Zprma", - "qqDgiB3dILkzdnxOIVeJvSaMnFa23V8j+vBtuFVxuH57RgWt2ZHNGuhmArVnj4jhJqPXBBrGFlIqlSeN", - "/9APQg/0TH+mK5FeQcaQIcnqcerviy5OOAl7gJuqmzD9Zr31BlVZgoTs4YyxE8lIiJwT2zvqepPLL8yu", - "+W9o1qymG0MuGS1ydinjfqK9b/xELvJgdvOOTcD5xKkskN0TmRs5wkB8Q+FyBBflyJ2hqXMaGei2gSoP", - "mMpicYj6/I6yUnhnl0VG1m6rvnS9KASlpgTdpqgr/G3h0F0SZsbYBUkLmqsarqFCf5xre8i7u/1CoNej", - "6zQFyI4vZdLBJFWFm/hB+18riJf10dFTYEcP+2O0QTvFWeZWBvpjv2JHU9tE5GJfscvJ5WQAqYJCXUNm", - "vZOQr+2ovWD/WwP3Uv40UEWs4Fvr13hZZLpeLkUqLNFzhZpspXrmhlTUAhWiB+gdaCbMlJQ3UZTMNLsv", - "rQDGj8f7cKAjUNFAw8OjqvjW3xF1eUczuOEprpKTktmyDTJKw2fDU86oMgkBRON8O2Z0EVh7E+qjIx8p", - "d/04yXRi3bnd+F30HLoOOQJ2ne032gbEiGJwiPifsFLhrguXDeJTBnKhzQBJ51lS+L1hyMihM2P/R9Us", - "5SS/ZW2gMepVRZYyeVA4A52ifk5nm7QUghwKsP42tTx61F/4o0duz4VmS9j4FCrs2CfHo0dWCJQ2nywB", - "Pda8OYuYDBTlxNM0kva65no92xvxJLgHBToD0GenfkISJq3piLmdTtDXyrf3IPAWEKvAWTi6E3XQtlUt", - "w3Qtt396qw0Uw9CZHfr3EdvrrXcRBietkrmQkBRKwjaaoSwk/EiN0XOaWGRkMAnr2Ni+C9XBv4dWd55D", - "dvNT6Uu7HbDEmyZ57B42vw+3FzUNE9XIyoS8ZJyluaCIlJLaVHVqLiUnD7lnBvXYwvv94zGTl75LPEgT", - "iaE4UJeSa6Rh4zdHo+lLiETEvgXwoRNdr1age2YRWwJcStdLSFZLYWgusioTu2ElVHTtMbM90RJY8pxC", - "PL9DpdiiNl3VS/k01rKxIVychqnlpeSG5cC1YT8KeXFD4Lzf43lGgtmo6qqhQtxuXYEELXQSvxn6zrZ+", - "z/XaLx87emXjBtsoJcJvk262BjoJu//3wV+O350kf+PJ70fJi/8+f//h2e3DR4Mfn9x+9dX/6/709Par", - "h3/5z9hOedxj2R4O87NTZ5acndLZ00ZvB7h/tuhjIWQSZTJ0FwohKWmwx1vsAZ6gnoEetnFgt+uX0txI", - "ZKRrnouMm49jh76KG8iilY4e13Q2ohdM8mt9H3N3ViopeXpFF66TlTDrejFLVTH35th8pRrTbJ5xKJSk", - "tmzOSzFH93Z+/XjP0fgJ+opF1BXlU9mbtCAfJmKWuiuOjoeEEO17AJtQhh7CKSyFFNh+fCkzbvh8wbVI", - "9bzWUH3Ncy5TmK0UO2YO5Ck3nBzrXjxo7MkOZTs7bMp6kYuUXYXnW8vvY/GVy8t3SPXLy/eD64nhaeSm", - "ijK+nSDZCLNWtUlcTG3cOW8DGATZhnd2zTplDrbdZhezc/Dj+o+XpU5ylfI80YYbiC+/LHNcfnBmakaD", - "KBuGaaMqr1lQ3bhAAe7va+UuaCq+8UnKNTrDvxS8fCekec8S59SelOUrhHmOePziBBi17raEjgNzYB5T", - "CyzmvNDCrZVy5wwpAnpuR/mHOjpOOWwi0lEfFLU2eP+xdEJQ36scN/ejyRTAiFKnNusEZSq6Ko2sRfIQ", - "PC3jK1Qw/kYFfVFkPvfUYQEsXUN6BRmFjSnwNu0M9xeZTl17kRXavk6wiVCUQks+1gJYXWbcHWhcbvu5", - "jBqM8Qmcb+EKtheqzcC9S/Li7XTiYsMJ8syYgJRIj0CzqmVXXHx8ubf5LjJO8duyZKtcLZxUNWxx3PCF", - "HzMuQFbd34PwxJiiIcMOfi95FSGEZf4REnzEQhHeJ7F+bHklr4xIRWnXf1jG5pvOGASyT6lH1bha9rX1", - "QJlGtbftnCy4jituwBbcD5Shfs6An8mGK7i906EXro5xFzkElxPaSTavyILwy7ZP9sZQi3MJVLI9TT0a", - "XYqEx/baXSqJ6/YqiS4TDzng9t5tIBf5W2DRjekKnDeHaz4aXh9NLT8LrnaDF0tN4rhXbH1hmDaPCOzj", - "YZ9g7rPKfSr5ZHqntPDpxGXwxLZDSTrdM8hhxV00mXKDHKM41L7QwQYhHj8tl+jzsyR2S8y1VqmwV2qt", - "LndzABp/jxiz0Qp2MIQYGwdoUxiOALPXKpRNuboLkhIExe24h00BvOBv2B/Gal9xO7Nyr/k31B2tEE3b", - "VxZ2G4chlekkqpLGLPNOL2a7LGDgH8RYFFXTMMgwDGVoyIGO46SjWZOrWOgJrQogNjz3wwJznT0QSzzk", - "HwbR2ApW6NC2TiBKq49qfF5H/FoZSJai0iYh/zO6POz0rSZj8FvsGlc/HVIx+wxUZHHtQ9NewTbJRF7H", - "d9vN+8MpTvu68Vt0vbiCLR0ywNM1W9CzZTyFOtNjnx1T20yJnQt+ZRf8it/beg/jJeyKE1dKmd4cfxCu", - "6umTXcIUYcAYcwx3bZSkO9QL+T6nkJtY0nnwSoS8SVSY9rXEqLc+EKbMw95lfgVYjGteCym6lsDQ3bkK", - "mz9iU0SCV7/DTNgRGeBlKbKbnu9soY7kSJABfwdD3Vr8AyrQ7jpgeygQ+MmxxLAKvK9vtzQ4M+37bRmu", - "bXYQZShBJyBIoBDCqYT21UeGhELWpify+2h1ATz/AbZ/xb60nMntdPJpLn+M1g7iHlq/abY3SmcKzFoX", - "sBM5uyPJeVlW6prniXtsMMaalbp2rEnd/duEz3+ApjnwygagduJM/cp/EZzRm42lMl0EUQ2yNL3fa42o", - "YOOa111hIMTnWnXsMNRAjjGsaDSHUyhGLjCyjN/t7A1z2AnaOOCdpSoE8MlRtSAomdyruA6kI85/7Q7v", - "kelwrh1vxQtbDkEzJfs3/miCkYdI7FLwLe6iDaoOhVvWRYIMnuhcpHG3Xy40yoisC8qh3xpg1HnEmEOI", - "tRgJfctaBLCwmz7g6qSHZDBHlJgUktlBu4VydaxqKX6rgYkMpMGmymUAdYQFZcOncQ6Po3jKqAPsskYb", - "8J9yRiOosdOZkNh9QIcR2kh+rnfY/EKb0DL+EATW7nDBEs44OFJ2XI44/nDcbK9+191Ia1h2aqiDkDFs", - "iYL9Na+827+2iI7MEa1hNaqxT8a1NaUCH66nW7VM6IYK2Sar8VyrCJhabri0JWlwnKWhG63B+tw4aqMq", - "ekaiIXplK3SyrNTvEPcEl7hRkaQkR0oyt2j0LJKe31eiTVSjLTbm6RviMcraY5ZQ0Mi6F2AjEk5cHoSe", - "KcvSB4i4tGxty+d07jLjwhHmH8wt/FY4HM6DnI2cbxY89nAcTRbE6aS95OiEsoxifrDfBd0kFzveC+5L", - "mr7Cvr0ooWozB+/NQPljsXwGqSh4Ho9sZkT97uu7TKyErUFUawiK3DhAtnib5SJXKMheI7WkOVuyo2lQ", - "RsvtRiauhRaLHKjHY9tjwTWdWk24shmCywNp1pq6Pzmg+7qWWQWZWWtLWK1YY0SSK9TEjhdgNgCSHVG/", - "xy/YA4qaa3END5GKzhaZHD9+QTkK9o+j2GHnio3t0isZKZb/5RRLnI/p2sDCwEPKQZ1F3wHZCpHjKmyH", - "NNmhh8gS9XRab78sFVzyFcRvQ4s9ONmxtJsUdOvRRWa2vJk2ldoyYeLzg+Gon0bylFD9WTRc8niBAmQU", - "06pAfmor2NhJPThbK80VkfB4+Ua6oij9I4Cew/l5fS17lsdWTRdJr3kBXbJOGbfP5egdg3tm6RTibOT1", - "PlTX8UmqkQ3256Ybyx5IJZMCZSd72GbABfwXfbyuDM+j0xqvu/pZJ7tBH2pqIZRklLB1h7A80EkfTeK6", - "iq+T1zjVz29fuYOhUFXsJXqrDd0hUYGpBFxHJbafydVYJs1x4SkfM1D8e/3fatAm9miGGmzuC/lteAba", - "t/oMZEYnyIzZRyaIdueZAGluUdS5TTmHbAWVc+rrMlc8mzKEc/HNyStmZ9XuRRw9bqBaASv7YKkhUSQE", - "FLzxPuxa3JcgiqfKHA5ndw4BrlobenipDS/KWGoh9rjwHSh/8ZqL3F9Hk0oLqTNjp/Y00V5X2Unap2ms", - "mc7xb75S9BSYG8PTNanpjlKzQhL1/Q4ucuGzc3VQLa4pvNU8nbZvz4zydS5smYspU3iWboS2FS/hGrrZ", - "jE1qrzMTfHZjd3lVLaXllLjO25F6/jFk98jZix4f5ohi1iP8HVWXVnWVwl1rfpzTqOhDln4BkUGZOAnZ", - "xY1sqjL5SsYpl0qKlJ6RBDU2G5Rd9cxD4nAHvLjpu2BexJ2ERoQrWrakuUp2VBwtZOIVoSPcMAgRtOKm", - "Wu6wfxoq04jOxQqMdpoNsqkvTeN8AyE1uKfwVEg10JPo4vXvk6Kh7vYV8B3ZiNLBRo7Ab7GNjj/hUjiu", - "hKQXgo5sLlvEWu9U3M+gyyAMWynQbj3dFzD6HY6ZXdzIM8T4/cwXAyQYNiyJy7ZR7iGoEx/zfuNqCKmK", - "vcS+jEKQ7c+d1DM76UlZukljmkA3OxwrrjNK4EhkNfGhrYC4DfwQ2g5223kVRecpMhpcUzAcSjqHB4wx", - "8s74G3SULEfZ54r2Cjia/y5kBI1XQkJbqjJyQKTRI4E2huR1ZJxOK27S9cE67QJ4TtH3mELTxoUjPhVU", - "b4OJJLRGP8f4NrYllkYUR9OhzU7ncttUyETuDoyJl1Sa1xFyWDCJrCpnRGWU5NMroRRTHKi4fVGy7gEw", - "FIOhTWSHm4pbybnLSTSWlJwJjSZuscgjaQ2nTWNQRozypxZb+jf2ynN8Be6y5s7X7f5mhgbe2b7sQhpY", - "h7j3iRarj9yVdvw9bktPBsI9inH/N6hWwkdngwe7VvE0RfToSlf5IpDkVDSJyl2eJUUXo0NQt2+3IzRe", - "gW9KqnEkseNt+yyPW+1r401j6R3paDYSNy7V0HC2qziFLY8Xg2DvtmxZPlsjP+psjt1n2essbB6MPsxu", - "GFhhBHsnQf1F6RChH3wWAyu5cMHUVkSGlHX5TsMMtEMyIdoN7i/CZRERkNhKPjLp5yDZG1IpItjhdfMe", - "9rzqkNS+DuhZkqqCeyZtcITekbTDi/RDl0frII6pNQzXefAGdGg7QvtDCN/qhSFxx8XZLA4R53iSNQ4n", - "fWIJ4p8BDLXJZ9MGnaqebt7Yrv91tCCafQfEDdsA41IqkigXdWOcFSqDnGlXHyOHFU+37uWevpQplywT", - "FVCRCVFQYS7O9IavVlDRk09bS9PHJghaZLdqkWf72MbB+Jr6Rl7S/jPfwg6F2CJ7J3Oiv7W00N1vP5tp", - "/lHvPVNVFDY00CF/9NVj85SKgi6EfltMblfscFFxaT2RAYUISlDHP1JVas2lhDw62t5N/JM4pOC/qhGc", - "CyHjTX0WsITpkaFdc3eFfkoPP1IGYTrRkNaVMFvKH/Keifh7NK/5u0Z+XSny5hbWXQLaz2K48Hgr7e2X", - "DL5Ttjhwge4SuQ6GKpd8c8OLMgenR7/6YvEnePrnZ9nR08d/Wvz56PlRCs+evzg64i+e8ccvnj6GJ39+", - "/uwIHi+/fLF4kj159mTx7MmzL5+/SJ8+e7x49uWLP33hPyNgEW1L9P9vKgWQnLw5Sy4Q2XajeCl+gK19", - "zYzc6cs18JQ0NxRc5JNj/9P/8HKCAhR8+cz9OnG3DZO1MaU+ns83m80sHDJfUfW4xKg6Xc/9PMNCMW/O", - "moC+TTogWbKxWhR0Oi+EySnThNrefnN+wU7enM1adTA5nhzNjmaPqXpHCZKXYnI8eUo/Edevad/na+C5", - "Qcm4nU7mBZhKpNr95VT4zFWqwJ+un8x9BHD+wV2t3+5q6+Y2uMcmwYDgteL8Q/BXIrIQLr3lm3/weR9B", - "ky3WOv9AAcbgd1dtcf6hLX96a7k7h1ikx1fnartT1S2qzK7tr8jQ/m5S6G4J2mZ3zjLcFRz1sikFG36R", - "8t1/0e+3ve991eLJ0dG/6/BTLc1nd6TETr+mEweIzPs1z5i/Y6S5H3++uc8kvQBBRcWsIr6dTp5/ztWf", - "SRQFnjPqGWSaDFniZ3kl1Ub6nnhq1kXBq60Xb91RFr7wM+lmvtJUJbAS19zA5D2VoYxd6o4oHfrgwZ2V", - "Dn3F4d9K53MpnT/25y3+rXT+aErn3CqFw5WOM4RsssfcVjNr7SP/5nD4EK9r2Y1pLmfoswcUVZaweegS", - "RizYyKPO5nJeZTaC5Avz+NQmN+tsoNneOqCd98M/wFbvU3MXa2C/tF/w/oUSMOmqZspUxX7heR78Rh9i", - "9CbsbORz4M1Dv0O/BX57O42htQTw6aCU9ukKcqK6vwL/JNTSoHOdO8yAaGujLWH0k6C2hFSo2RwLPj46", - "Ooq9rOjj7KJdFmNKv92oJIdryIdbPYZE72Xorg/ojX7NYPigN/Q6I1znvzfbvPEd/Z5g95XqXbA7VfIL", - "wzZcuErYQVUY+82IQhj/qU2bUuVS+JqzI/55xgRB7v5666cecX+8Apu3O5SdXtcmUxs5rrjofQ/PXYIs", - "paw2zrZRzANoNNWM+U+l5Vv/8U/GKblL1ab7TV5f7KFXR7gpR7QSkiYgKadZbCY4D/Is3ecEhkrw3GH2", - "2n59oaf3op8mtDjG5T4m9J/KS4cbIDv30BcN6fw9R1FAY89+yiUhyg3dfgM8n7t0n96v9lI++LFbQzjy", - "67x5dBVt7AczYq3zD+bGxSuCwBttWRNye/ceKU/pvG432zjS8XxON99rpc18gpqnG2MKG983RP3gWcAT", - "9/b97f8PAAD//8QAO4wngwAA", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 003f5390fe..a05ca47160 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -561,159 +561,160 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "AP//GQiLU1LXAAA=", } // GetSwagger returns the Swagger specification corresponding to the generated code From 07ef499dff2b0f4ad31fbbf61723e4ce7b4c3091 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 22 Jul 2020 21:35:17 -0400 Subject: [PATCH 138/267] Improve TestArchivalCreatables, remove obsolete interface function. --- ledger/acctupdates.go | 6 ------ ledger/archival_test.go | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index a04962a80a..08c224e78a 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -591,12 +591,6 @@ func (aul *accountUpdatesLedgerEvaluator) isDup(config.ConsensusParams, basics.R return false, fmt.Errorf("accountUpdatesLedgerEvaluator: tried to check for dup during accountUpdates initilization ") } -// GetRoundTxIds returns the transaction ids for a given round. It's not being used by the evaluator at all, and should be (future task) be removed from -// the ledgerForEvaluator interface -func (aul *accountUpdatesLedgerEvaluator) GetRoundTxIds(rnd basics.Round) (txMap map[transactions.Txid]bool) { - return nil -} - // Lookup returns the account balance for a given address at a given round, without the reward func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { return aul.au.lookupImpl(rnd, addr, false) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 0abf3a546a..fca2784a0d 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -359,6 +359,14 @@ func TestArchivalCreatables(t *testing.T) { // keep track of max created idx var maxCreated uint64 + // wait for the written blocks to get flushed every so often. + // this is not needed by the test itself, but rather helps avoiding + // false positive failuires as disk writes on travis tend to be extreamly + // slow. since this test requires disk writes, we want to flush the blocks + // periodically so that the account updates won't get blocked beyond the + // deadlock library triggeting interval. + blocksFlushInterval := 128 + // create apps and assets for i := 0; i < maxBlocks; i++ { blk.BlockHeader.Round++ @@ -414,6 +422,10 @@ func TestArchivalCreatables(t *testing.T) { // Add the block err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) + + if i > blocksFlushInterval { + l.WaitForCommit(basics.Round(i - blocksFlushInterval)) + } } l.WaitForCommit(blk.Round()) @@ -596,6 +608,9 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) + if i > blocksFlushInterval { + l.WaitForCommit(basics.Round(i - blocksFlushInterval)) + } } l.WaitForCommit(blk.Round()) From 8913e3996a6a88a79cab922caf73316675ce2e0b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 22 Jul 2020 21:58:58 -0400 Subject: [PATCH 139/267] refactor blocksFlushInterval for other tests. --- ledger/archival_test.go | 25 +++++++++++++++++-------- ledger/ledger_test.go | 4 ++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index fca2784a0d..e70a3ad0c3 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -42,6 +42,14 @@ import ( "github.com/algorand/go-algorand/protocol" ) +// wait for the written blocks to get flushed every so often. +// this is not needed by the test itself, but rather helps avoiding +// false positive failuires as disk writes on travis tend to be extreamly +// slow. since this test requires disk writes, we want to flush the blocks +// periodically so that the account updates won't get blocked beyond the +// deadlock library triggeting interval. +var blocksFlushInterval = 128 + type wrappedLedger struct { l *Ledger minQueriedBlock basics.Round @@ -189,6 +197,10 @@ func TestArchivalRestart(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) + + if i > blocksFlushInterval { + l.WaitForCommit(basics.Round(i - blocksFlushInterval)) + } } l.WaitForCommit(blk.Round()) @@ -359,14 +371,6 @@ func TestArchivalCreatables(t *testing.T) { // keep track of max created idx var maxCreated uint64 - // wait for the written blocks to get flushed every so often. - // this is not needed by the test itself, but rather helps avoiding - // false positive failuires as disk writes on travis tend to be extreamly - // slow. since this test requires disk writes, we want to flush the blocks - // periodically so that the account updates won't get blocked beyond the - // deadlock library triggeting interval. - blocksFlushInterval := 128 - // create apps and assets for i := 0; i < maxBlocks; i++ { blk.BlockHeader.Round++ @@ -608,6 +612,7 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) + if i > blocksFlushInterval { l.WaitForCommit(basics.Round(i - blocksFlushInterval)) } @@ -694,6 +699,10 @@ func TestArchivalFromNonArchival(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) + + if i > blocksFlushInterval { + l.WaitForCommit(basics.Round(i - blocksFlushInterval)) + } } l.WaitForCommit(blk.Round()) diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 7ca813bdc5..35657038fc 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -885,6 +885,10 @@ func TestLedgerBlockHdrCaching(t *testing.T) { hdr, err := l.BlockHdr(blk.BlockHeader.Round) require.NoError(t, err) require.Equal(t, blk.BlockHeader, hdr) + + if i > blocksFlushInterval { + l.WaitForCommit(basics.Round(i - blocksFlushInterval)) + } } } From 6234b07f88835e101407e0b566197e512843360b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 22 Jul 2020 23:06:06 -0400 Subject: [PATCH 140/267] update for travis test. --- ledger/acctupdates.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 08c224e78a..662f16f41a 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -23,6 +23,7 @@ import ( "io" "os" "path/filepath" + "runtime/pprof" "sort" "strconv" "sync" @@ -382,7 +383,13 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b defer func() { au.accountsMu.RUnlock() if dc.offset != 0 { - au.committedOffset <- dc + //au.committedOffset <- dc + select { + case au.committedOffset <- dc: + case <-time.After(10 * time.Second): + pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) + panic("accountUpdates was waiting for too long") + } } }() From cb6e403e53feec2f84b8f413f626b3bb454817f2 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Thu, 23 Jul 2020 10:28:04 -0400 Subject: [PATCH 141/267] -- Addressing comments --- cmd/algokey/common.go | 28 ++++++++++++++++++++++++++++ cmd/algokey/multisig.go | 23 +++++++++++++++-------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/cmd/algokey/common.go b/cmd/algokey/common.go index 2bccef2f68..947b34b497 100644 --- a/cmd/algokey/common.go +++ b/cmd/algokey/common.go @@ -25,6 +25,11 @@ import ( "github.com/algorand/go-algorand/crypto/passphrase" ) +const ( + stdoutFilenameValue = "-" + stdinFileNameValue = "-" +) + func loadKeyfileOrMnemonic(keyfile string, mnemonic string) crypto.Seed { if keyfile != "" && mnemonic != "" { fmt.Fprintf(os.Stderr, "Cannot specify both keyfile and mnemonic\n") @@ -94,3 +99,26 @@ func computeMnemonic(seed crypto.Seed) string { } return mnemonic } + +// writeFile is a wrapper of ioutil.WriteFile which considers the special +// case of stdout filename +func writeFile(filename string, data []byte, perm os.FileMode) error { + var err error + if filename == stdoutFilenameValue { + // Write to Stdout + if _, err = os.Stdout.Write(data); err != nil { + return err + } + return nil + } + return ioutil.WriteFile(filename, data, perm) +} + +// readFile is a wrapper of ioutil.ReadFile which considers the +// special case of stdin filename +func readFile(filename string) ([]byte, error) { + if filename == stdinFileNameValue { + return ioutil.ReadAll(os.Stdin) + } + return ioutil.ReadFile(filename) +} diff --git a/cmd/algokey/multisig.go b/cmd/algokey/multisig.go index ade690fe1b..b056426481 100644 --- a/cmd/algokey/multisig.go +++ b/cmd/algokey/multisig.go @@ -51,7 +51,10 @@ func init() { multisigCmd.MarkFlagRequired("outfile") convertCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig params - Threshold PK1 PK2 ...") + convertCmd.MarkFlagRequired("params") convertCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") + convertCmd.MarkFlagRequired("txfile") + convertCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename. If not specified, the original file will be modified") } @@ -108,12 +111,12 @@ var multisigCmd = &cobra.Command{ var convertCmd = &cobra.Command{ Use: "convert -t [transaction file] -p \"[threshold] [PK1] [PK2] ...\"", - Short: "Adds the necessary fields to a transaction that is sent from an account to was rekeied to a multisig account.", + Short: "Adds the necessary fields to a transaction that is sent from an account that was rekeyed to a multisig account", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, _ []string) { // Read Transaction - txdata, err := ioutil.ReadFile(multisigTxfile) + txdata, err := readFile(multisigTxfile) if err != nil { fmt.Fprintf(os.Stderr, "Cannot read transactions from %s: %v\n", multisigTxfile, err) os.Exit(1) @@ -132,7 +135,7 @@ var convertCmd = &cobra.Command{ // Decode params params := strings.Split(msigParams, " ") if len(params) < 2 { - _, _ = fmt.Fprint(os.Stderr, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and addresses") + fmt.Fprint(os.Stderr, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and addresses") os.Exit(1) } @@ -142,8 +145,7 @@ var convertCmd = &cobra.Command{ os.Exit(1) } - // convert addresses to pks - // convert the addresses into public keys + // Convert the addresses into public keys pks := make([]crypto.PublicKey, len(params[1:])) for i, addrStr := range params[1:] { addr, err := basics.UnmarshalChecksumAddress(addrStr) @@ -160,10 +162,10 @@ var convertCmd = &cobra.Command{ os.Exit(1) } - //gen the multisig and assign to the txn + // Generate the multisig and assign to the txn stxn.Msig = crypto.MultisigPreimageFromPKs(1, uint8(threshold), pks) - //append the signer since it's a rekey txn + // Append the signer since it's a rekey txn if basics.Address(addr) == stxn.Txn.Sender { fmt.Fprintf(os.Stderr, "The sender at the msig address should not be the same: %v\n", err) os.Exit(1) @@ -173,7 +175,12 @@ var convertCmd = &cobra.Command{ // Write the txn outBytes = append(outBytes, protocol.Encode(&stxn)...) - err = ioutil.WriteFile(multisigTxfile, outBytes, 0600) + // Check if we should override the current file or create a new one + if multisigOutfile == "" { + multisigOutfile = multisigTxfile + } + + err = writeFile(multisigOutfile, outBytes, 0600) if err != nil { fmt.Fprintf(os.Stderr, "Cannot write transactions to %s: %v\n", multisigOutfile, err) os.Exit(1) From 7cdf23a93298638db5afbf0a24a97a737df7f902 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Thu, 23 Jul 2020 10:54:10 -0400 Subject: [PATCH 142/267] -- sort imports --- cmd/algokey/multisig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/algokey/multisig.go b/cmd/algokey/multisig.go index b056426481..2f25111677 100644 --- a/cmd/algokey/multisig.go +++ b/cmd/algokey/multisig.go @@ -18,7 +18,6 @@ package main import ( "fmt" - "github.com/algorand/go-algorand/data/basics" "io" "io/ioutil" "os" @@ -28,6 +27,7 @@ import ( "github.com/spf13/cobra" "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" ) From bfabe4541532f0361885d1fc376395baa2065b59 Mon Sep 17 00:00:00 2001 From: egieseke Date: Thu, 23 Jul 2020 11:26:06 -0400 Subject: [PATCH 143/267] Stateful teal test app info (#1291) Implemented a Stateful teal app info test using expect --- .../cli/goal/expect/goalExpectCommon.exp | 34 +++- .../goal/expect/statefulTealAppInfoTest.exp | 158 ++++++++++++++++++ .../goal/expect/statefulTealAppReadTest.exp | 145 ++++++++++++++++ 3 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp create mode 100644 test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp diff --git a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp index 4b4b2522e5..ea240d6afe 100755 --- a/test/e2e-go/cli/goal/expect/goalExpectCommon.exp +++ b/test/e2e-go/cli/goal/expect/goalExpectCommon.exp @@ -896,10 +896,29 @@ proc ::AlgorandGoal::TakeAccountOnline { ADDRESS FIRST_ROUND LAST_ROUND TEST_PRI } } -# stateful teal app procedures +# Stateful Teal App Procedures +# App Create with 0 arguments +proc ::AlgorandGoal::AppCreate0 { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APPROVAL_PROGRAM GLOBAL_BYTE_SLICES LOCAL_BYTE_SLICES CLEAR_PROGRAM DATA_DIR } { + set timeout 60 + if { [ catch { + set APP_ID "NOT SET" + puts "calling goal app create" + spawn goal app create --creator $ACCOUNT_ADDRESS --approval-prog $APPROVAL_PROGRAM --global-byteslices $GLOBAL_BYTE_SLICES --global-ints 0 --local-byteslices $LOCAL_BYTE_SLICES --local-ints 0 --clear-prog $CLEAR_PROGRAM -w $WALLET_NAME -d $DATA_DIR + expect { + timeout { abort "\n Failed to see expected output" } + "Please enter the password for wallet '$WALLET_NAME':" {send "$WALLET_PASSWORD\r" ; exp_continue } + -re {Created app with app index ([0-9]+)} {set APP_ID $expect_out(1,string) ;close } + eof {close; ::AlgorandGoal::Abort "app not created" } + } + puts "App ID $APP_ID" + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCreate: $EXCEPTION" + } + return $APP_ID +} -# App Create +# App Create with 1 argument proc ::AlgorandGoal::AppCreate { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APPROVAL_PROGRAM APP_ARG GLOBAL_BYTE_SLICES LOCAL_BYTE_SLICES CLEAR_PROGRAM DATA_DIR } { set timeout 60 if { [ catch { @@ -919,7 +938,6 @@ proc ::AlgorandGoal::AppCreate { WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP return $APP_ID } - # App OptIn with 1 argument proc ::AlgorandGoal::AppOptIn { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDRESS APP_ARG1 DATA_DIR } { set timeout 60 @@ -1005,6 +1023,16 @@ proc ::AlgorandGoal::AppDelete { APP_ID WALLET_NAME WALLET_PASSWORD ACCOUNT_ADDR } } +# Compile teal program +proc ::AlgorandGoal::AppCompile { INPUT_PROGRAM COMPILED_OUTPUT DATA_DIR } { + set timeout 60 + if { [ catch { + puts "calling goal clerk compile for $INPUT_PROGRAM" + exec goal clerk compile ${INPUT_PROGRAM} -o ${COMPILED_OUTPUT} -d ${DATA_DIR} + } EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in AppCompile: $EXCEPTION" + } +} # CheckEOF checks if there was an error, and aborts if there was an error proc ::AlgorandGoal::CheckEOF { { ERROR_STRING "" } } { diff --git a/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp b/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp new file mode 100644 index 0000000000..ff44fad3cb --- /dev/null +++ b/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp @@ -0,0 +1,158 @@ +#!/usr/bin/expect -f +#exp_internal 1 +set err 0 +log_user 1 + +source goalExpectCommon.exp + +set TEST_ALGO_DIR [lindex $argv 0] +set TEST_DATA_DIR [lindex $argv 1] + +proc statefulTealAppInfoTest { TEST_ALGO_DIR TEST_DATA_DIR} { + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] + puts "Primary Node Address: $PRIMARY_NODE_ADDRESS" + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + # Determine primary account + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Check the balance of the primary account + set PRIMARY_ACCOUNT_BALANCE [::AlgorandGoal::GetAccountBalance $PRIMARY_WALLET_NAME $PRIMARY_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR] + puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" + + ::AlgorandGoal::WaitForRound 1 $TEST_PRIMARY_NODE_DIR + + set TEAL_PROGS_DIR "$TEST_DATA_DIR/../scripts/e2e_subs/tealprogs" + + # Network Setup complete + #---------------------- + + puts "calling app compile" + ::AlgorandGoal::AppCompile ${TEAL_PROGS_DIR}/upgraded.teal ${TEST_ROOT_DIR}/upgraded.tealc $TEST_PRIMARY_NODE_DIR + puts "computing target hash" + + puts "compute target hash" + set TARGET_HASH [ exec shasum -a 256 "${TEST_ROOT_DIR}/upgraded.tealc" | awk {{print $1}} ] + puts "TARGET_HASH ${TARGET_HASH}" + + # Compile dummy, wrong contract + ::AlgorandGoal::AppCompile ${TEAL_PROGS_DIR}/wrongupgrade.teal ${TEST_ROOT_DIR}/wrongupgrade.tealc $TEST_PRIMARY_NODE_DIR + + # Copy template + exec cp ${TEAL_PROGS_DIR}/bootloader.teal.tmpl ${TEST_ROOT_DIR}/bootloader.teal + + # Substitute template values + exec sed -i"" -e "s/TMPL_APPROV_HASH/${TARGET_HASH}/g" ${TEST_ROOT_DIR}/bootloader.teal + exec sed -i"" -e "s/TMPL_CLEARSTATE_HASH/${TARGET_HASH}/g" ${TEST_ROOT_DIR}/bootloader.teal + + # Create an app using filled-in bootloader template + puts "calling app create" + set GLOBAL_BYTE_SLICES 1 + set LOCAL_BYTE_SLICES 0 + set APP_ID [::AlgorandGoal::AppCreate0 $PRIMARY_WALLET_NAME "" $PRIMARY_ACCOUNT_ADDRESS ${TEST_ROOT_DIR}/bootloader.teal $GLOBAL_BYTE_SLICES $LOCAL_BYTE_SLICES ${TEAL_PROGS_DIR}/clear_program_state.teal $TEST_PRIMARY_NODE_DIR] + + # Application setup complete + #---------------------- + + # Calling app as an update but with right scripts should succeed + spawn goal app info --app-id $APP_ID -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + -re {^Application ID:\s+(\d+)\r\n} {set APP_INFO_ID $expect_out(1,string) ; exp_continue } + -re {Creator:\s+([A-Z0-9]+)\r\n} {set APP_INFO_CREATOR $expect_out(1,string) ; exp_continue } + -re {Approval hash:\s+([A-Z0-9]+)\r\n} {set APP_INFO_APPROVAL_HASH $expect_out(1,string); exp_continue } + -re {Clear hash:\s+([A-Z0-9]+)\r\n} {set APP_INFO_CLEAR_HASH $expect_out(1,string); exp_continue } + -re {Max global byteslices:\s+(\d+)\r\n} {set APP_INFO_GLOBAL_BYTESLICES $expect_out(1,string); exp_continue } + -re {Max global integers:\s+(\d+)\r\n} {set APP_INFO_GLOBAL_INTEGERS $expect_out(1,string) ; exp_continue } + -re {Max local byteslices:\s+(\d+)\r\n} {set APP_INFO_LOCAL_BYTESLICES $expect_out(1,string) ; exp_continue } + -re {Max local integers:\s+(\d+)\r\n} {set APP_INFO_LOCAL_INTEGERS $expect_out(1,string) ; close } + eof {close; ::AlgorandGoal::Abort "app update failed" } + } + puts "APP_INFO_ID $APP_INFO_ID" + puts "APP_INFO_CREATOR $APP_INFO_CREATOR" + puts "APP_INFO_APPROVAL_HASH $APP_INFO_APPROVAL_HASH" + puts "APP_INFO_CLEAR_HASH $APP_INFO_CLEAR_HASH" + puts "APP_INFO_GLOBAL_BYTESLICES $APP_INFO_GLOBAL_BYTESLICES" + puts "APP_INFO_GLOBAL_INTEGERS $APP_INFO_GLOBAL_INTEGERS" + puts "APP_INFO_LOCAL_BYTESLICES $APP_INFO_LOCAL_BYTESLICES" + puts "APP_INFO_LOCAL_INTEGERS $APP_INFO_LOCAL_INTEGERS" + + set errors 0 + if { $APP_INFO_ID != $APP_ID } { + puts "error APP_INFO_ID $APP_INFO_ID does not match expected $APP_ID" ; incr errors + } + if { $APP_INFO_CREATOR != $PRIMARY_ACCOUNT_ADDRESS } { + puts "error APP_INFO_CREATOR $APP_INFO_CREATOR does not match expected $PRIMARY_ACCOUNT_ADDRESS" ; incr errors + } + set EXPECTED_APP_INFO_APPROVAL_HASH "AJM7G3WXKKL6YTITFNRYT53HRFKHKWGTEZF6UZXKSUNO6GI7FOBCA7LDTU" + if { $APP_INFO_APPROVAL_HASH != "AJM7G3WXKKL6YTITFNRYT53HRFKHKWGTEZF6UZXKSUNO6GI7FOBCA7LDTU" } { + puts "error APP_INFO_APPROVAL_HASH $APP_INFO_APPROVAL_HASH does not match expected $EXPECTED_APP_INFO_APPROVAL_HASH" ; incr errors + } + set EXPECTED_APP_INFO_CLEAR_HASH "YOE6C22GHCTKAN3HU4SE5PGIPN5UKXAJTXCQUPJ3KKF5HOAH646MKKCPDA" + if { $APP_INFO_CLEAR_HASH != $EXPECTED_APP_INFO_CLEAR_HASH } { + puts "error APP_INFO_CLEAR_HASH $APP_INFO_CLEAR_HASH does not match expected $EXPECTED_APP_INFO_CLEAR_HASH" ; incr errors + } + set EXPECTED_APP_INFO_GLOBAL_BYTESLICES 1 + if { $APP_INFO_GLOBAL_BYTESLICES != $EXPECTED_APP_INFO_GLOBAL_BYTESLICES } { + puts "error APP_INFO_GLOBAL_BYTESLICES $APP_INFO_GLOBAL_BYTESLICES does not match expected $EXPECTED_APP_INFO_GLOBAL_BYTESLICES" ; incr errors + } + set EXPECTED_APP_INFO_GLOBAL_INTEGERS 0 + if { $APP_INFO_GLOBAL_INTEGERS != $EXPECTED_APP_INFO_GLOBAL_INTEGERS } { + puts "error APP_INFO_GLOBAL_INTEGERS $APP_INFO_GLOBAL_INTEGERS does not match expected $EXPECTED_APP_INFO_GLOBAL_INTEGERS" ; incr errors + } + set EXPECTED_APP_INFO_LOCAL_BYTESLICES 0 + if { $APP_INFO_LOCAL_BYTESLICES != $EXPECTED_APP_INFO_LOCAL_BYTESLICES } { + puts "error APP_INFO_LOCAL_BYTESLICES $APP_INFO_LOCAL_BYTESLICES does not match expected $EXPECTED_APP_INFO_LOCAL_BYTESLICES" ; incr errors + } + set EXPECTED_APP_INFO_LOCAL_INTEGERS 0 + if { $APP_INFO_LOCAL_INTEGERS != $EXPECTED_APP_INFO_LOCAL_INTEGERS } { + puts "error APP_INFO_LOCAL_INTEGERS $APP_INFO_LOCAL_INTEGERS does not match expected $EXPECTED_APP_INFO_LOCAL_INTEGERS" ; incr errors + } + + if { $errors > 0 } { + puts "there were a total of $errors" + ::AlgorandGoal::Abort "ERROR in statefulTealAppInfoTest" + } else { + puts "app info test was successful" + } + + # Shutdown the network + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + + puts "Goal statefulTealAppInfoTest Successful" + +} + + +if { [catch { + source goalExpectCommon.exp + + puts "starting statefulTealAppInfoTest" + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + statefulTealAppInfoTest $TEST_ALGO_DIR $TEST_DATA_DIR + + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in statefulTealAppInfoTest: $EXCEPTION" +} diff --git a/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp b/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp new file mode 100644 index 0000000000..12f3f9a688 --- /dev/null +++ b/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp @@ -0,0 +1,145 @@ +#!/usr/bin/expect -f +#exp_internal 1 +set err 0 +log_user 1 + +source goalExpectCommon.exp + +set TEST_ALGO_DIR [lindex $argv 0] +set TEST_DATA_DIR [lindex $argv 1] + +proc statefulTealAppReadTest { TEST_ALGO_DIR TEST_DATA_DIR} { + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + # Start network + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] + puts "Primary Node Address: $PRIMARY_NODE_ADDRESS" + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + # Determine primary account + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + # Check the balance of the primary account + set PRIMARY_ACCOUNT_BALANCE [::AlgorandGoal::GetAccountBalance $PRIMARY_WALLET_NAME $PRIMARY_ACCOUNT_ADDRESS $TEST_PRIMARY_NODE_DIR] + puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" + + ::AlgorandGoal::WaitForRound 1 $TEST_PRIMARY_NODE_DIR + + set TEAL_PROGS_DIR "$TEST_DATA_DIR/../scripts/e2e_subs/tealprogs" + + # Network Setup complete + #---------------------- + + puts "calling app compile" + ::AlgorandGoal::AppCompile ${TEAL_PROGS_DIR}/upgraded.teal ${TEST_ROOT_DIR}/upgraded.tealc $TEST_PRIMARY_NODE_DIR + puts "computing target hash" + + puts "compute target hash" + set TARGET_HASH [ exec shasum -a 256 "${TEST_ROOT_DIR}/upgraded.tealc" | awk {{print $1}} ] + puts "TARGET_HASH ${TARGET_HASH}" + + # Compile dummy, wrong contract + ::AlgorandGoal::AppCompile ${TEAL_PROGS_DIR}/wrongupgrade.teal ${TEST_ROOT_DIR}/wrongupgrade.tealc $TEST_PRIMARY_NODE_DIR + + # Copy template + exec cp ${TEAL_PROGS_DIR}/bootloader.teal.tmpl ${TEST_ROOT_DIR}/bootloader.teal + + # Substitute template values + exec sed -i"" -e "s/TMPL_APPROV_HASH/${TARGET_HASH}/g" ${TEST_ROOT_DIR}/bootloader.teal + exec sed -i"" -e "s/TMPL_CLEARSTATE_HASH/${TARGET_HASH}/g" ${TEST_ROOT_DIR}/bootloader.teal + + # Create an app using filled-in bootloader template + puts "calling app create" + set GLOBAL_BYTE_SLICES 1 + set LOCAL_BYTE_SLICES 0 + set APP_ID [::AlgorandGoal::AppCreate0 $PRIMARY_WALLET_NAME "" $PRIMARY_ACCOUNT_ADDRESS ${TEST_ROOT_DIR}/bootloader.teal $GLOBAL_BYTE_SLICES $LOCAL_BYTE_SLICES ${TEAL_PROGS_DIR}/clear_program_state.teal $TEST_PRIMARY_NODE_DIR] + + # Calling app without args and wrong OnCompletion should fail + puts "calling goal app call with no args" + spawn goal app call --app-id $APP_ID --from $PRIMARY_ACCOUNT_ADDRESS -w $PRIMARY_WALLET_NAME -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Couldn't broadcast tx with algod: HTTP 400 Bad Request: TransactionPool.Remember: transaction*" {puts "received expected error"; close} + eof { close; ::AlgorandGoal::Abort "did not receive expected error" } + } + + # Calling app as an update but with wrong scripts should fail + puts "calling goal app call with wrong scripts" + spawn goal app call --app-id $APP_ID --from $PRIMARY_ACCOUNT_ADDRESS -w $PRIMARY_WALLET_NAME --approval-prog-raw ${TEAL_PROGS_DIR}/wrongupgrade.tealc --clear-prog-raw ${TEAL_PROGS_DIR}/wrongupgrade.tealc -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Couldn't broadcast tx with algod: HTTP 400 Bad Request: TransactionPool.Remember: transaction*" {puts "received expected error"; close} + eof { close; ::AlgorandGoal::Abort "did not receive expected error" } + } + + # Calling app as an update but with right scripts should succeed + spawn goal app update --app-id $APP_ID --from $PRIMARY_ACCOUNT_ADDRESS -w $PRIMARY_WALLET_NAME -d $TEST_PRIMARY_NODE_DIR --approval-prog-raw ${TEST_ROOT_DIR}/upgraded.tealc --clear-prog-raw ${TEST_ROOT_DIR}/upgraded.tealc + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$PRIMARY_WALLET_NAME':" {send "$PRIMARY_WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "app update succeeded"; close} + eof {close; ::AlgorandGoal::Abort "app update failed" } + } + + # Global state should be empty + spawn goal app read --guess-format --app-id $APP_ID --global -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + -re {^\{\}$} {puts "app read succeeded"} + eof {close; ::AlgorandGoal::Abort "app read failed" } + } + + # Calling app should succeed + spawn goal app call --app-id $APP_ID --from $PRIMARY_ACCOUNT_ADDRESS -w $PRIMARY_WALLET_NAME -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "Please enter the password for wallet '$PRIMARY_WALLET_NAME':" {send "$PRIMARY_WALLET_PASSWORD\r" ; exp_continue} + "*committed in round*" {puts "app call succeeded"} + eof {close; ::AlgorandGoal::Abort "app call failed" } + } + + # Global state should now have 'foo': 'foo' key + spawn goal app read --guess-format --app-id $APP_ID --global -d $TEST_PRIMARY_NODE_DIR + expect { + timeout { puts timeout; ::AlgorandGoal::Abort "\n Failed to see expected output" } + "*foo*" {puts "app read succeeded"} + eof {close; ::AlgorandGoal::Abort "app read failed" } + } + + # Shutdown the network + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + + puts "Goal statefulTealAppReadTest Successful" + +} + + +if { [catch { + source goalExpectCommon.exp + + puts "starting statefulTealAppReadTest" + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + statefulTealAppReadTest $TEST_ALGO_DIR $TEST_DATA_DIR + + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in statefulTealAppReadTest: $EXCEPTION" +} From 0aa8b8b5c1b3824b112d26e272318b0eb1d11690 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 12:01:54 -0400 Subject: [PATCH 144/267] instument for timing tests. --- ledger/acctupdates.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 662f16f41a..db27fa0234 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -379,6 +379,7 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b var isCatchpointRound, hasMultipleIntermediateCatchpoint bool var offset uint64 var dc deferedCommit + fmt.Fprintf(os.Stdout, "committedUpTo(%d) before RLock : %v\n", committedRound, time.Now()) au.accountsMu.RLock() defer func() { au.accountsMu.RUnlock() @@ -387,6 +388,7 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b select { case au.committedOffset <- dc: case <-time.After(10 * time.Second): + fmt.Fprintf(os.Stdout, "committedUpTo write to channel timeout : %v\n", time.Now()) pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) panic("accountUpdates was waiting for too long") } @@ -1224,6 +1226,7 @@ func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookback basics.Round) { defer au.accountsWriting.Done() + fmt.Fprintf(os.Stdout, "commitRound(%d) before RLock : %v\n", dbRound, time.Now()) au.accountsMu.RLock() // we can exit right away, as this is the result of mis-ordered call to committedUpTo. @@ -1294,7 +1297,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb var catchpointLabel string beforeUpdatingBalancesTime := time.Now() var trieBalancesHash crypto.Digest - + fmt.Fprintf(os.Stdout, "commitRound(%d) before AtomicCommitWriteLock : %v\n", dbRound, time.Now()) err := au.dbs.wdb.AtomicCommitWriteLock(func(ctx context.Context, tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) if au.catchpointInterval > 0 { @@ -1315,6 +1318,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { + fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v\n", dbRound, uint64(dbRound)+i, time.Now()) err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) if err != nil { return err @@ -1336,7 +1340,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb return } } - + fmt.Fprintf(os.Stdout, "commitRound(%d) before exit AtomicCommitWriteLock : %v\n", dbRound, time.Now()) return nil }, &au.accountsMu) From a78b707b0e40aa5cf11cb0a2eba8312dd3383094 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 13:10:56 -0400 Subject: [PATCH 145/267] more precise logging. : --- ledger/acctupdates.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index db27fa0234..e691a06c61 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1318,16 +1318,21 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { - fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v\n", dbRound, uint64(dbRound)+i, time.Now()) + + t1 := time.Now() err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) if err != nil { return err } + d1 := time.Now().Sub(t1) + t1 = time.Now() err = au.accountsUpdateBalances(deltas[i]) if err != nil { return err } + d2 := time.Now().Sub(t1) + fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v d1 = %v d2 = %v\n", dbRound, uint64(dbRound)+i, time.Now(), d1, d2) } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) if err != nil { From de85d035525fa6dd2d5ee744b71331162a8ceafe Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Thu, 23 Jul 2020 14:44:11 -0400 Subject: [PATCH 146/267] -- convert to append-auth-addr --- cmd/algokey/multisig.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/algokey/multisig.go b/cmd/algokey/multisig.go index 2f25111677..fed16636dd 100644 --- a/cmd/algokey/multisig.go +++ b/cmd/algokey/multisig.go @@ -41,7 +41,7 @@ var msigParams string func init() { - multisigCmd.AddCommand(convertCmd) + multisigCmd.AddCommand(appendAuthAddrCmd) multisigCmd.Flags().StringVarP(&multisigKeyfile, "keyfile", "k", "", "Private key filename") multisigCmd.Flags().StringVarP(&multisigMnemonic, "mnemonic", "m", "", "Private key mnemonic") @@ -50,11 +50,11 @@ func init() { multisigCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename") multisigCmd.MarkFlagRequired("outfile") - convertCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig params - Threshold PK1 PK2 ...") - convertCmd.MarkFlagRequired("params") - convertCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") - convertCmd.MarkFlagRequired("txfile") - convertCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename. If not specified, the original file will be modified") + appendAuthAddrCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig pre image parameters - [threshold] [Address 1] [Address 2]") + appendAuthAddrCmd.MarkFlagRequired("params") + appendAuthAddrCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") + appendAuthAddrCmd.MarkFlagRequired("txfile") + appendAuthAddrCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename. If not specified, the original file will be modified") } @@ -109,8 +109,8 @@ var multisigCmd = &cobra.Command{ }, } -var convertCmd = &cobra.Command{ - Use: "convert -t [transaction file] -p \"[threshold] [PK1] [PK2] ...\"", +var appendAuthAddrCmd = &cobra.Command{ + Use: "append-auth-addr -t [transaction file] -p \"[threshold] [Address 1] [Address 2] ...\"", Short: "Adds the necessary fields to a transaction that is sent from an account that was rekeyed to a multisig account", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, _ []string) { @@ -134,8 +134,8 @@ var convertCmd = &cobra.Command{ // Decode params params := strings.Split(msigParams, " ") - if len(params) < 2 { - fmt.Fprint(os.Stderr, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and addresses") + if len(params) < 3 { + fmt.Fprint(os.Stderr, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and at least 2 addresses\n") os.Exit(1) } From a09ec000283b334153638c1e6530246f56b22166 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 16:21:55 -0400 Subject: [PATCH 147/267] Add some more time-testings. --- ledger/acctupdates.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index e691a06c61..50db1da24f 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1333,6 +1333,9 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } d2 := time.Now().Sub(t1) fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v d1 = %v d2 = %v\n", dbRound, uint64(dbRound)+i, time.Now(), d1, d2) + if (d1 + d2) > 2*time.Second { + os.Exit(1) + } } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) if err != nil { From 3011f759544d0a3475fea10df783bfcb9f82f549 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 16:57:20 -0400 Subject: [PATCH 148/267] update --- ledger/acctupdates.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 50db1da24f..5c07afef5e 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1042,7 +1042,12 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Addre } } // write it all to disk. + t := time.Now() err = au.balancesTrie.Commit() + d := time.Now().Sub(t) + if d > 100*time.Millisecond { + fmt.Fprintf(os.Stdout, "%v : accountsUpdateBalances tool %v\n", time.Now(), d) + } return } @@ -1318,7 +1323,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { - + fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v\n", dbRound, uint64(dbRound)+i, time.Now()) t1 := time.Now() err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) if err != nil { @@ -1332,9 +1337,9 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb return err } d2 := time.Now().Sub(t1) - fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v d1 = %v d2 = %v\n", dbRound, uint64(dbRound)+i, time.Now(), d1, d2) if (d1 + d2) > 2*time.Second { - os.Exit(1) + fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v d1 = %v d2 = %v\n", dbRound, uint64(dbRound)+i, time.Now(), d1, d2) + panic(nil) } } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) From 424d2e6e5812563eeecf847a8ca38b97de372773 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 20:58:41 -0400 Subject: [PATCH 149/267] rollback few testing changes. --- ledger/acctupdates.go | 29 ++--------------------------- ledger/archival_test.go | 24 ------------------------ ledger/ledger_test.go | 4 ---- 3 files changed, 2 insertions(+), 55 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 5c07afef5e..70c6d2df03 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -23,7 +23,6 @@ import ( "io" "os" "path/filepath" - "runtime/pprof" "sort" "strconv" "sync" @@ -379,19 +378,11 @@ func (au *accountUpdates) committedUpTo(committedRound basics.Round) (retRound b var isCatchpointRound, hasMultipleIntermediateCatchpoint bool var offset uint64 var dc deferedCommit - fmt.Fprintf(os.Stdout, "committedUpTo(%d) before RLock : %v\n", committedRound, time.Now()) au.accountsMu.RLock() defer func() { au.accountsMu.RUnlock() if dc.offset != 0 { - //au.committedOffset <- dc - select { - case au.committedOffset <- dc: - case <-time.After(10 * time.Second): - fmt.Fprintf(os.Stdout, "committedUpTo write to channel timeout : %v\n", time.Now()) - pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) - panic("accountUpdates was waiting for too long") - } + au.committedOffset <- dc } }() @@ -1042,12 +1033,7 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Addre } } // write it all to disk. - t := time.Now() err = au.balancesTrie.Commit() - d := time.Now().Sub(t) - if d > 100*time.Millisecond { - fmt.Fprintf(os.Stdout, "%v : accountsUpdateBalances tool %v\n", time.Now(), d) - } return } @@ -1231,7 +1217,6 @@ func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookback basics.Round) { defer au.accountsWriting.Done() - fmt.Fprintf(os.Stdout, "commitRound(%d) before RLock : %v\n", dbRound, time.Now()) au.accountsMu.RLock() // we can exit right away, as this is the result of mis-ordered call to committedUpTo. @@ -1302,7 +1287,7 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb var catchpointLabel string beforeUpdatingBalancesTime := time.Now() var trieBalancesHash crypto.Digest - fmt.Fprintf(os.Stdout, "commitRound(%d) before AtomicCommitWriteLock : %v\n", dbRound, time.Now()) + err := au.dbs.wdb.AtomicCommitWriteLock(func(ctx context.Context, tx *sql.Tx) (err error) { treeTargetRound := basics.Round(0) if au.catchpointInterval > 0 { @@ -1323,24 +1308,15 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { - fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v\n", dbRound, uint64(dbRound)+i, time.Now()) - t1 := time.Now() err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) if err != nil { return err } - d1 := time.Now().Sub(t1) - t1 = time.Now() err = au.accountsUpdateBalances(deltas[i]) if err != nil { return err } - d2 := time.Now().Sub(t1) - if (d1 + d2) > 2*time.Second { - fmt.Fprintf(os.Stdout, "commitRound(%d) writing round %d : %v d1 = %v d2 = %v\n", dbRound, uint64(dbRound)+i, time.Now(), d1, d2) - panic(nil) - } } err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) if err != nil { @@ -1353,7 +1329,6 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb return } } - fmt.Fprintf(os.Stdout, "commitRound(%d) before exit AtomicCommitWriteLock : %v\n", dbRound, time.Now()) return nil }, &au.accountsMu) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index e70a3ad0c3..0abf3a546a 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -42,14 +42,6 @@ import ( "github.com/algorand/go-algorand/protocol" ) -// wait for the written blocks to get flushed every so often. -// this is not needed by the test itself, but rather helps avoiding -// false positive failuires as disk writes on travis tend to be extreamly -// slow. since this test requires disk writes, we want to flush the blocks -// periodically so that the account updates won't get blocked beyond the -// deadlock library triggeting interval. -var blocksFlushInterval = 128 - type wrappedLedger struct { l *Ledger minQueriedBlock basics.Round @@ -197,10 +189,6 @@ func TestArchivalRestart(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) - - if i > blocksFlushInterval { - l.WaitForCommit(basics.Round(i - blocksFlushInterval)) - } } l.WaitForCommit(blk.Round()) @@ -426,10 +414,6 @@ func TestArchivalCreatables(t *testing.T) { // Add the block err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - - if i > blocksFlushInterval { - l.WaitForCommit(basics.Round(i - blocksFlushInterval)) - } } l.WaitForCommit(blk.Round()) @@ -612,10 +596,6 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - - if i > blocksFlushInterval { - l.WaitForCommit(basics.Round(i - blocksFlushInterval)) - } } l.WaitForCommit(blk.Round()) @@ -699,10 +679,6 @@ func TestArchivalFromNonArchival(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) - - if i > blocksFlushInterval { - l.WaitForCommit(basics.Round(i - blocksFlushInterval)) - } } l.WaitForCommit(blk.Round()) diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 35657038fc..7ca813bdc5 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -885,10 +885,6 @@ func TestLedgerBlockHdrCaching(t *testing.T) { hdr, err := l.BlockHdr(blk.BlockHeader.Round) require.NoError(t, err) require.Equal(t, blk.BlockHeader, hdr) - - if i > blocksFlushInterval { - l.WaitForCommit(basics.Round(i - blocksFlushInterval)) - } } } From 1071c8860d03b6894f8a7afcc3e6c84e65444f59 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 21:45:59 -0400 Subject: [PATCH 150/267] optimize accountsUpdateBalances so that travis unit test would stop failing. --- ledger/acctupdates.go | 66 ++++++++++++++++++++++++++++--------------- ledger/totals_test.go | 2 +- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 4b1d355164..656d50fc75 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -49,6 +49,9 @@ const ( // trieRebuildAccountChunkSize defines the number of accounts that would get read at a single chunk // before added to the trie during trie construction trieRebuildAccountChunkSize = 512 + // trieAccumulatedChangesFlush defines the number of pending changes that would be applied to the merkle trie before + // we attempt to commit them to disk while writing a batch of rounds balances to disk. + trieAccumulatedChangesFlush = 256 ) // trieCachedNodesCount defines how many balances trie nodes we would like to keep around in memory. @@ -1036,35 +1039,54 @@ func (au *accountUpdates) deleteStoredCatchpoints(ctx context.Context, dbQueries return nil } -func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Address]accountDelta) (err error) { +// accountsUpdateBalances applies the given deltas array to the merkle trie +func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basics.Address]accountDelta, offset uint64) (err error) { if au.catchpointInterval == 0 { return nil } var added, deleted bool - for addr, delta := range accountsDeltas { - if !delta.old.IsZero() { - deleteHash := accountHashBuilder(addr, delta.old, protocol.Encode(&delta.old)) - deleted, err = au.balancesTrie.Delete(deleteHash) - if err != nil { - return err + accumulatedChanges := 0 + for i := uint64(0); i < offset; i++ { + accountsDeltas := accountsDeltasRound[i] + for addr, delta := range accountsDeltas { + if !delta.old.IsZero() { + deleteHash := accountHashBuilder(addr, delta.old, protocol.Encode(&delta.old)) + deleted, err = au.balancesTrie.Delete(deleteHash) + if err != nil { + return err + } + if !deleted { + au.log.Warnf("failed to delete hash '%v' from merkle trie", deleteHash) + } else { + accumulatedChanges++ + } + } - if !deleted { - au.log.Warnf("failed to delete hash '%v' from merkle trie", deleteHash) + if !delta.new.IsZero() { + addHash := accountHashBuilder(addr, delta.new, protocol.Encode(&delta.new)) + added, err = au.balancesTrie.Add(addHash) + if err != nil { + return err + } + if !added { + au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie", addHash) + } else { + accumulatedChanges++ + } } } - if !delta.new.IsZero() { - addHash := accountHashBuilder(addr, delta.new, protocol.Encode(&delta.new)) - added, err = au.balancesTrie.Add(addHash) + if accumulatedChanges >= trieAccumulatedChangesFlush { + accumulatedChanges -= trieAccumulatedChangesFlush + err = au.balancesTrie.Commit() if err != nil { - return err - } - if !added { - au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie", addHash) + return } } } // write it all to disk. - err = au.balancesTrie.Commit() + if accumulatedChanges > 0 { + err = au.balancesTrie.Commit() + } return } @@ -1211,12 +1233,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb if err != nil { return err } - - err = au.accountsUpdateBalances(deltas[i]) - if err != nil { - return err - } } + err = au.accountsUpdateBalances(deltas, offset) + if err != nil { + return err + } + err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) if err != nil { return err diff --git a/ledger/totals_test.go b/ledger/totals_test.go index ac05e5f22f..1d9cfdf0b6 100644 --- a/ledger/totals_test.go +++ b/ledger/totals_test.go @@ -69,7 +69,7 @@ func TestAlgoCountMarshalMsg(t *testing.T) { inBuffer := make([]byte, 0, 128) outBuffer, err := ac.MarshalMsg(inBuffer) require.NoError(t, err) - fmt.Printf("out %d in %d\n", len(outBuffer), len(inBuffer)) + //fmt.Printf("out %d in %d\n", len(outBuffer), len(inBuffer)) require.True(t, len(outBuffer) > len(inBuffer)) // allocate a buffer that is just the right size. From 237437c860a5d5034e1391a26259c1c4af7cbb14 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 22:09:33 -0400 Subject: [PATCH 151/267] Fix broken unit test. --- ledger/totals_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ledger/totals_test.go b/ledger/totals_test.go index 1d9cfdf0b6..911eb87eed 100644 --- a/ledger/totals_test.go +++ b/ledger/totals_test.go @@ -17,7 +17,6 @@ package ledger import ( - "fmt" "testing" "github.com/stretchr/testify/require" From ab32a26f5fa76a1f453e5c45ba679ca1ac64d632 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 23 Jul 2020 22:33:08 -0400 Subject: [PATCH 152/267] reset to zero after committing. --- ledger/acctupdates.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 656d50fc75..e047b10e03 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1076,7 +1076,7 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basic } } if accumulatedChanges >= trieAccumulatedChangesFlush { - accumulatedChanges -= trieAccumulatedChangesFlush + accumulatedChanges = 0 err = au.balancesTrie.Commit() if err != nil { return From b04f4b68dafc623d36d228ef48311e8600374e5e Mon Sep 17 00:00:00 2001 From: Will Winder Date: Fri, 24 Jul 2020 10:23:49 -0400 Subject: [PATCH 153/267] Add missing enum option to tx-type parameter. (#1292) This is purely a documentation/spec change, I forgot to add "appl" to the list of tx-types --- daemon/algod/api/algod.oas2.json | 3 +- daemon/algod/api/algod.oas3.yml | 3 +- .../api/server/v2/generated/private/routes.go | 208 +++++++------- .../algod/api/server/v2/generated/routes.go | 268 +++++++++--------- 4 files changed, 242 insertions(+), 240 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 3e2981b9bc..15edc838bf 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -1910,7 +1910,8 @@ "keyreg", "acfg", "axfer", - "afrz" + "afrz", + "appl" ], "type": "string", "name": "tx-type", diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 2bdc68d9c8..c375753a11 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -215,7 +215,8 @@ "keyreg", "acfg", "axfer", - "afrz" + "afrz", + "appl" ], "type": "string" } diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 78e3b2720b..445b005dd8 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -255,110 +255,110 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index a05ca47160..5e8f24a4d1 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -581,140 +581,140 @@ var swaggerSpec = []string{ "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", - "AP//GQiLU1LXAAA=", + "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", + "HwAA//8lx6pdWdcAAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code From df265379eec24186729e77ace37a4d735a9f8e39 Mon Sep 17 00:00:00 2001 From: bricerisingalgorand <60147418+bricerisingalgorand@users.noreply.github.com> Date: Fri, 24 Jul 2020 10:25:10 -0400 Subject: [PATCH 154/267] Add test for when the user does not provide a correct application json (#1286) This adds a test for goal clerk dryrun failures. Right now it just tests the failure case of not providing an application file, but it includes a method that can test any error message scenario we can expect --- .../cli/goal/expect/goalDryrunRestTest.exp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index abb028096b..9cf5344898 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -21,6 +21,25 @@ proc TestGoalDryrun { DRREQ_FILE TEST_PRIMARY_NODE_DIR } { } } +proc TestGoalDryrunExitCode { DRREQ_FILE TEST_PRIMARY_NODE_DIR EXPECTED_STATUS_CODE EXPECTED_MESSAGE} { + set MESSAGE_MATCHED 0 + spawn goal clerk dryrun-remote -d $TEST_PRIMARY_NODE_DIR -D $DRREQ_FILE -v + expect { + timeout { ::AlgorandGoal::Abort "goal clerk dryrun-remote timeout" } + $EXPECTED_MESSAGE {puts "message matched"; set MESSAGE_MATCHED 1; close} + eof + } + catch wait result + set STATUS_CODE [lindex $result 3] + if { $STATUS_CODE != $EXPECTED_STATUS_CODE } { + puts "Exit code: $STATUS_CODE, expected: $EXPECTED_STATUS_CODE" + ::AlgorandGoal::Abort "Progran exited with incorrect code" + } + if { $MESSAGE_MATCHED == 0 } { + ::AlgorandGoal::Abort "Progam message did not match expected" + } +} + if { [catch { source goalExpectCommon.exp @@ -85,9 +104,19 @@ if { [catch { timeout { ::AlgorandGoal::Abort "goal app create timeout" } } + # invalid app + set INVALID_FILE_1 "$TEST_ROOT_DIR/invalid-app.json" + set INVALID_FILE_1_ID [open $INVALID_FILE_1 "w"] + set INVALID_FILE_1_DATA "{ \"round\": -1 }" + puts -nonewline $INVALID_FILE_1_ID $INVALID_FILE_1_DATA + close $INVALID_FILE_1_ID + TestGoalDryrun $DRREQ_FILE_1 $TEST_PRIMARY_NODE_DIR TestGoalDryrun $DRREQ_FILE_2 $TEST_PRIMARY_NODE_DIR TestGoalDryrun $DRREQ_FILE_3 $TEST_PRIMARY_NODE_DIR + TestGoalDryrunExitCode $DRREQ_FILE_3 $TEST_PRIMARY_NODE_DIR 0 "PASS" + TestGoalDryrunExitCode "" $TEST_PRIMARY_NODE_DIR 1 "Cannot read file : open : no such file or directory" + TestGoalDryrunExitCode $INVALID_FILE_1 $TEST_PRIMARY_NODE_DIR 1 "dryrun-remote: HTTP 400 Bad Request:" # Shutdown the network ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR From d9add9c3be32988952abe35c75f6875e3ce1493f Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 24 Jul 2020 12:57:00 -0400 Subject: [PATCH 155/267] Implement expect test for goal account dump (#1290) Test goal account dump produces JSON or msgpack blob, and errs on invalid input --- cmd/goal/account.go | 2 +- .../cli/algoh/expect/algohExpectCommon.exp | 2 +- .../cli/goal/expect/goalAccountTest.exp | 141 ++++++++++++++++++ .../cli/goal/expect/goalDryrunRestTest.exp | 5 +- 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100644 test/e2e-go/cli/goal/expect/goalAccountTest.exp diff --git a/cmd/goal/account.go b/cmd/goal/account.go index 4429195555..6fe26f647e 100644 --- a/cmd/goal/account.go +++ b/cmd/goal/account.go @@ -559,7 +559,7 @@ var dumpCmd = &cobra.Command{ data := protocol.Encode(&br) writeFile(dumpOutFile, data, 0644) } else { - data := protocol.EncodeJSON(&br) + data := protocol.EncodeJSONStrict(&br) fmt.Println(string(data)) } }, diff --git a/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp b/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp index 0fb4dec439..71c8e26747 100644 --- a/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp +++ b/test/e2e-go/cli/algoh/expect/algohExpectCommon.exp @@ -62,7 +62,7 @@ proc ::Algoh::StartNode { TEST_ALGO_DIR } { spawn goal node start -d $TEST_ALGO_DIR expect { timeout { close; ::Algoh::Abort "Timed out starting relay node" } - "^Algorand node successfully started!* { puts "Primary relay node started"; close } + "^Algorand node successfully started!*" { puts "Primary relay node started"; close } eof { catch wait result; if { [lindex $result 3] != 0 } { puts "Unable to start node"; Algoh::Abort } } } } EXCEPTION ] } { diff --git a/test/e2e-go/cli/goal/expect/goalAccountTest.exp b/test/e2e-go/cli/goal/expect/goalAccountTest.exp new file mode 100644 index 0000000000..b581368c26 --- /dev/null +++ b/test/e2e-go/cli/goal/expect/goalAccountTest.exp @@ -0,0 +1,141 @@ +#!/usr/bin/expect -f +set err 0 +log_user 1 + +if { [catch { + source goalExpectCommon.exp + set TEST_ALGO_DIR [lindex $argv 0] + set TEST_DATA_DIR [lindex $argv 1] + + puts "TEST_ALGO_DIR: $TEST_ALGO_DIR" + puts "TEST_DATA_DIR: $TEST_DATA_DIR" + + set TIME_STAMP [clock seconds] + + set TEST_ROOT_DIR $TEST_ALGO_DIR/root + set TEST_PRIMARY_NODE_DIR $TEST_ROOT_DIR/Primary/ + set NETWORK_NAME test_net_expect_$TIME_STAMP + set NETWORK_TEMPLATE "$TEST_DATA_DIR/nettemplates/TwoNodes50EachFuture.json" + + exec cp $TEST_DATA_DIR/../../gen/devnet/genesis.json $TEST_ALGO_DIR + + # Create network + ::AlgorandGoal::CreateNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + ::AlgorandGoal::StartNetwork $NETWORK_NAME $NETWORK_TEMPLATE $TEST_ALGO_DIR $TEST_ROOT_DIR + + set PRIMARY_NODE_ADDRESS [ ::AlgorandGoal::GetAlgodNetworkAddress $TEST_PRIMARY_NODE_DIR ] + puts "Primary Node Address: $PRIMARY_NODE_ADDRESS" + + set PRIMARY_WALLET_NAME unencrypted-default-wallet + + # Determine primary account + set PRIMARY_ACCOUNT_ADDRESS [::AlgorandGoal::GetHighestFundedAccountForWallet $PRIMARY_WALLET_NAME $TEST_PRIMARY_NODE_DIR] + + set MN "advice pudding treat near rule blouse same whisper inner electric quit surface sunny dismiss leader blood seat clown cost exist hospital century reform able sponsor" + spawn goal account import -m $MN --datadir $TEST_PRIMARY_NODE_DIR + expect { + timeout { close; ::AlgorandGoal::Abort "goal account import timeout" } + -re {Imported ([A-Z0-9]+)} {set NEW_ACCOUNT_ADDRESS $expect_out(1,string); close } + eof { ::AlgorandGoal::CheckEOF "Failed to import account" } + } + + puts "Imported account $NEW_ACCOUNT_ADDRESS" + + set MIN_BALANCE 100000 + set TRANSFER_AMOUNT [expr {3 * $MIN_BALANCE}] + set FEE_AMOUNT 1000 + set TRANSACTION_ID [::AlgorandGoal::AccountTransfer $PRIMARY_WALLET_NAME "" $PRIMARY_ACCOUNT_ADDRESS $TRANSFER_AMOUNT $NEW_ACCOUNT_ADDRESS $FEE_AMOUNT $TEST_PRIMARY_NODE_DIR] + + set GLOBAL_BYTE_SLICES 1 + set LOCAL_BYTE_SLICES 0 + set TEAL_PROGS_DIR "$TEST_DATA_DIR/../scripts/e2e_subs/tealprogs" + set APP_ID [::AlgorandGoal::AppCreate0 $PRIMARY_WALLET_NAME "" $NEW_ACCOUNT_ADDRESS ${TEAL_PROGS_DIR}/clear_program_state.teal $GLOBAL_BYTE_SLICES $LOCAL_BYTE_SLICES ${TEAL_PROGS_DIR}/clear_program_state.teal $TEST_PRIMARY_NODE_DIR] + + # expect app idx = 2 since a pre-recorded response is checked down the road + if { $APP_ID != 2 } { + ::AlgorandGoal::Abort "Expected app id to be 2 but got $APP_ID. Have you posted additional transactions? Only transfer txn is expected before app call txn" + } + + # check JSON output to stdout + set JSON_EXPECTED "{ + \"addr\": \"47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU\", + \"algo\": 299000, + \"appp\": { + \"2\": { + \"approv\": \"AiABASI=\", + \"clearp\": \"AiABASI=\", + \"gsch\": { + \"nbs\": 1 + } + } + }, + \"tsch\": { + \"nbs\": 1 + } +}" + + set JSON_ACTUAL [exec goal account dump -a $NEW_ACCOUNT_ADDRESS --datadir $TEST_PRIMARY_NODE_DIR] + if { $JSON_ACTUAL != $JSON_EXPECTED } { + ::AlgorandGoal::Abort "json actual output '$JSON_ACTUAL' does not match expected '$JSON_EXPECTED'" + } + + # check msgpack output to a file with zero exit code + set MSGP_EXPECTED_BASE64 "hKRhZGRyxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaRhbGdvzgAEj/ikYXBwcIECg6ZhcHByb3bEBQIgAQEipmNsZWFycMQFAiABASKkZ3NjaIGjbmJzAaR0c2NogaNuYnMB" + set MSGP_EXPECTED [ exec echo -n $MSGP_EXPECTED_BASE64 | base64 --decode ] + set BALREC_FILE "$TEST_ROOT_DIR/brec.msgp" + spawn goal account dump -a $NEW_ACCOUNT_ADDRESS -o $BALREC_FILE --datadir $TEST_PRIMARY_NODE_DIR + expect { + timeout { close; ::AlgorandGoal::Abort "goal account dump timeout" } + eof { ::AlgorandGoal::CheckEOF "Failed to dump account" } + } + set MSGP_ACTUAL [exec cat "$BALREC_FILE"] + if { $MSGP_ACTUAL != $MSGP_EXPECTED } { + ::AlgorandGoal::Abort "msgp actual output '$MSGP_ACTUAL' does not match expected '$MSGP_EXPECTED'" + } + + # check some empty response on non-existing address with zero exit code + set PASSED 0 + set NONEXISTING_ADDR "42NJMHTPFVPXVSDGA6JGKUV6TARV5UZTMPFIREMLXHETRKIVW34QFSDFRE" + spawn goal account dump -a $NONEXISTING_ADDR --datadir $TEST_PRIMARY_NODE_DIR + expect { + timeout { close; ::AlgorandGoal::Abort "goal account dump timeout" } + "\"addr\": \"$NONEXISTING_ADDR\"" { set PASSED 1; close;} + eof { ::AlgorandGoal::CheckEOF "Failed to dump account" } + } + if { $PASSED == 0 } { + ::AlgorandGoal::Abort "Non-existing addr... Failed" + } + + # check failure if no address provided, non-zero exit code + set PASSED 0 + spawn goal account dump --datadir $TEST_PRIMARY_NODE_DIR + expect { + timeout { close; ::AlgorandGoal::Abort "goal account dump timeout" } + "^Failed to parse addr: decoded bad addr:" { set PASSED 1; close;} + eof { catch wait result; if { [lindex $result 3] == 0 } { puts "Expected non-zero exit code"; AlgorandGoal::Abort } } + } + if { $PASSED == 0 } { + ::AlgorandGoal::Abort "No addr... Failed" + } + + # check failure if invalid address provided, non-zero exit code + set PASSED 0 + set INVALID_ADDR "NOTANADDR" + spawn goal account dump -a $INVALID_ADDR --datadir $TEST_PRIMARY_NODE_DIR + expect { + timeout { close; ::AlgorandGoal::Abort "goal account dump timeout" } + "^Failed to parse addr: decoded bad addr:" { set PASSED 1; close;} + eof { catch wait result; if { [lindex $result 3] == 0 } { puts "Expected non-zero exit code"; AlgorandGoal::Abort } } + } + if { $PASSED == 0 } { + ::AlgorandGoal::Abort "Invalid addr... Failed" + } + + # Shutdown the network + ::AlgorandGoal::StopNetwork $NETWORK_NAME $TEST_ALGO_DIR $TEST_ROOT_DIR + exit 0 + +} EXCEPTION ] } { + ::AlgorandGoal::Abort "ERROR in goalAccountTest: $EXCEPTION" +} diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index 9cf5344898..72f608d793 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -1,5 +1,4 @@ #!/usr/bin/expect -f -#exp_internal 1 set err 0 log_user 1 @@ -9,8 +8,8 @@ proc TestGoalDryrun { DRREQ_FILE TEST_PRIMARY_NODE_DIR } { spawn goal clerk dryrun-remote -d $TEST_PRIMARY_NODE_DIR -D $DRREQ_FILE -v expect { timeout { ::AlgorandGoal::Abort "goal clerk dryrun-remote timeout" } - -re {(ApprovalProgram)} {puts "match1"; set PROGRAM_TYPE $expect_out(1,string); exp_continue} - "PASS" {puts "match2"; set PASSED 1; close} + -re {(ApprovalProgram)} {set PROGRAM_TYPE $expect_out(1,string); exp_continue} + "PASS" {set PASSED 1; close} } if { $PASSED == 0 } { ::AlgorandGoal::Abort "Program did not pass" From e7ba71c4644b8f3851867a77c44912257b949a97 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 24 Jul 2020 13:00:40 -0400 Subject: [PATCH 156/267] update TestAlgoCountMarshalMsg --- ledger/totals_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ledger/totals_test.go b/ledger/totals_test.go index 911eb87eed..9239d2f095 100644 --- a/ledger/totals_test.go +++ b/ledger/totals_test.go @@ -68,8 +68,7 @@ func TestAlgoCountMarshalMsg(t *testing.T) { inBuffer := make([]byte, 0, 128) outBuffer, err := ac.MarshalMsg(inBuffer) require.NoError(t, err) - //fmt.Printf("out %d in %d\n", len(outBuffer), len(inBuffer)) - require.True(t, len(outBuffer) > len(inBuffer)) + require.Truef(t, len(outBuffer) > len(inBuffer), "len(outBuffer) : %d\nlen(inBuffer): %d\n", len(outBuffer), len(inBuffer)) // allocate a buffer that is just the right size. inBuffer = make([]byte, len(outBuffer)) From a8237a240a393f0bfcd22ab42b850c5223565a14 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 24 Jul 2020 15:44:37 -0400 Subject: [PATCH 157/267] Optimize accountsUpdateBalances implementation (#1296) Existing code was calling accountsUpdateBalances for each round, applying the deltas to the trie and committing the changes to disk. However, on some of our testings, we have 1-2 changes per block, and we're pushing 2000-4000 blocks very quickly. This leads to a scenario where commitRound is asked to flush 500+ rounds to disk in a single batch. From logical perspective, there is nothing wrong here. --- ledger/acctupdates.go | 66 ++++++++++++++++++++++++++++--------------- ledger/totals_test.go | 4 +-- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 4b1d355164..e047b10e03 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -49,6 +49,9 @@ const ( // trieRebuildAccountChunkSize defines the number of accounts that would get read at a single chunk // before added to the trie during trie construction trieRebuildAccountChunkSize = 512 + // trieAccumulatedChangesFlush defines the number of pending changes that would be applied to the merkle trie before + // we attempt to commit them to disk while writing a batch of rounds balances to disk. + trieAccumulatedChangesFlush = 256 ) // trieCachedNodesCount defines how many balances trie nodes we would like to keep around in memory. @@ -1036,35 +1039,54 @@ func (au *accountUpdates) deleteStoredCatchpoints(ctx context.Context, dbQueries return nil } -func (au *accountUpdates) accountsUpdateBalances(accountsDeltas map[basics.Address]accountDelta) (err error) { +// accountsUpdateBalances applies the given deltas array to the merkle trie +func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basics.Address]accountDelta, offset uint64) (err error) { if au.catchpointInterval == 0 { return nil } var added, deleted bool - for addr, delta := range accountsDeltas { - if !delta.old.IsZero() { - deleteHash := accountHashBuilder(addr, delta.old, protocol.Encode(&delta.old)) - deleted, err = au.balancesTrie.Delete(deleteHash) - if err != nil { - return err + accumulatedChanges := 0 + for i := uint64(0); i < offset; i++ { + accountsDeltas := accountsDeltasRound[i] + for addr, delta := range accountsDeltas { + if !delta.old.IsZero() { + deleteHash := accountHashBuilder(addr, delta.old, protocol.Encode(&delta.old)) + deleted, err = au.balancesTrie.Delete(deleteHash) + if err != nil { + return err + } + if !deleted { + au.log.Warnf("failed to delete hash '%v' from merkle trie", deleteHash) + } else { + accumulatedChanges++ + } + } - if !deleted { - au.log.Warnf("failed to delete hash '%v' from merkle trie", deleteHash) + if !delta.new.IsZero() { + addHash := accountHashBuilder(addr, delta.new, protocol.Encode(&delta.new)) + added, err = au.balancesTrie.Add(addHash) + if err != nil { + return err + } + if !added { + au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie", addHash) + } else { + accumulatedChanges++ + } } } - if !delta.new.IsZero() { - addHash := accountHashBuilder(addr, delta.new, protocol.Encode(&delta.new)) - added, err = au.balancesTrie.Add(addHash) + if accumulatedChanges >= trieAccumulatedChangesFlush { + accumulatedChanges = 0 + err = au.balancesTrie.Commit() if err != nil { - return err - } - if !added { - au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie", addHash) + return } } } // write it all to disk. - err = au.balancesTrie.Commit() + if accumulatedChanges > 0 { + err = au.balancesTrie.Commit() + } return } @@ -1211,12 +1233,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb if err != nil { return err } - - err = au.accountsUpdateBalances(deltas[i]) - if err != nil { - return err - } } + err = au.accountsUpdateBalances(deltas, offset) + if err != nil { + return err + } + err = updateAccountsRound(tx, dbRound+basics.Round(offset), treeTargetRound) if err != nil { return err diff --git a/ledger/totals_test.go b/ledger/totals_test.go index ac05e5f22f..9239d2f095 100644 --- a/ledger/totals_test.go +++ b/ledger/totals_test.go @@ -17,7 +17,6 @@ package ledger import ( - "fmt" "testing" "github.com/stretchr/testify/require" @@ -69,8 +68,7 @@ func TestAlgoCountMarshalMsg(t *testing.T) { inBuffer := make([]byte, 0, 128) outBuffer, err := ac.MarshalMsg(inBuffer) require.NoError(t, err) - fmt.Printf("out %d in %d\n", len(outBuffer), len(inBuffer)) - require.True(t, len(outBuffer) > len(inBuffer)) + require.Truef(t, len(outBuffer) > len(inBuffer), "len(outBuffer) : %d\nlen(inBuffer): %d\n", len(outBuffer), len(inBuffer)) // allocate a buffer that is just the right size. inBuffer = make([]byte, len(outBuffer)) From 3251abee98659bbeeaad6ecc6e79842fb2a32d04 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 24 Jul 2020 16:01:18 -0400 Subject: [PATCH 158/267] Optimize updating account totals --- ledger/accountdb.go | 55 +++++++++++++++++++++++++--------------- ledger/accountdb_test.go | 4 ++- ledger/acctupdates.go | 7 ++++- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 8ebfffaa04..ebab6231bf 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -618,38 +618,35 @@ func accountsPutTotals(tx *sql.Tx, totals AccountTotals, catchpointStaging bool) return err } -func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creatables map[basics.CreatableIndex]modifiedCreatable, rewardsLevel uint64, proto config.ConsensusParams) (err error) { - var ot basics.OverflowTracker - totals, err := accountsTotals(tx, false) - if err != nil { - return - } +func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creatables map[basics.CreatableIndex]modifiedCreatable) (err error) { - totals.applyRewards(rewardsLevel, &ot) + var insertCreatableIdxStmt, deleteCreatableIdxStmt, deleteStmt, replaceStmt *sql.Stmt - deleteStmt, err := tx.Prepare("DELETE FROM accountbase WHERE address=?") + deleteStmt, err = tx.Prepare("DELETE FROM accountbase WHERE address=?") if err != nil { return } defer deleteStmt.Close() - replaceStmt, err := tx.Prepare("REPLACE INTO accountbase (address, data) VALUES (?, ?)") + replaceStmt, err = tx.Prepare("REPLACE INTO accountbase (address, data) VALUES (?, ?)") if err != nil { return } defer replaceStmt.Close() - insertCreatableIdxStmt, err := tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") - if err != nil { - return - } - defer insertCreatableIdxStmt.Close() + if len(creatables) > 0 { + insertCreatableIdxStmt, err = tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") + if err != nil { + return + } + defer insertCreatableIdxStmt.Close() - deleteCreatableIdxStmt, err := tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") - if err != nil { - return + deleteCreatableIdxStmt, err = tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") + if err != nil { + return + } + defer deleteCreatableIdxStmt.Close() } - defer deleteCreatableIdxStmt.Close() for addr, data := range updates { if data.new.IsZero() { @@ -662,8 +659,6 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creat return } - totals.delAccount(proto, data.old, &ot) - totals.addAccount(proto, data.new, &ot) } for cidx, cdelta := range creatables { @@ -677,6 +672,26 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creat } } + return +} + +// totalsNewRounds updates the accountsTotals by applying series of round changes +func totalsNewRounds(tx *sql.Tx, updates []map[basics.Address]accountDelta, accountTotals []AccountTotals, protos []config.ConsensusParams) (err error) { + var ot basics.OverflowTracker + totals, err := accountsTotals(tx, false) + if err != nil { + return + } + + for i := 0; i < len(updates); i++ { + totals.applyRewards(accountTotals[i].RewardsLevel, &ot) + + for _, data := range updates[i] { + totals.delAccount(protos[i], data.old, &ot) + totals.addAccount(protos[i], data.new, &ot) + } + } + if ot.Overflowed { err = fmt.Errorf("overflow computing totals") return diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index b331a4db18..8f38261063 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -206,7 +206,9 @@ func TestAccountDBRound(t *testing.T) { for i := 1; i < 10; i++ { updates, newaccts, _ := randomDeltas(20, accts, 0) accts = newaccts - err = accountsNewRound(tx, updates, nil, 0, proto) + err = accountsNewRound(tx, updates, nil) + require.NoError(t, err) + err = totalsNewRounds(tx, []map[basics.Address]accountDelta{updates}, []AccountTotals{AccountTotals{}}, []config.ConsensusParams{proto}) require.NoError(t, err) err = updateAccountsRound(tx, basics.Round(i), 0) require.NoError(t, err) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index e047b10e03..1725bf94f3 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1229,11 +1229,16 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb treeTargetRound = dbRound + basics.Round(offset) } for i := uint64(0); i < offset; i++ { - err = accountsNewRound(tx, deltas[i], creatableDeltas[i], roundTotals[i+1].RewardsLevel, protos[i+1]) + err = accountsNewRound(tx, deltas[i], creatableDeltas[i]) if err != nil { return err } } + err = totalsNewRounds(tx, deltas[:offset], roundTotals[1:offset+1], protos[1:offset+1]) + if err != nil { + return err + } + err = au.accountsUpdateBalances(deltas, offset) if err != nil { return err From c4353bbdae21a19b645b32715e9b517b263b4d10 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 24 Jul 2020 16:35:14 -0400 Subject: [PATCH 159/267] stage better. --- ledger/accountdb.go | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index ebab6231bf..122ee72d05 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -634,20 +634,6 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creat } defer replaceStmt.Close() - if len(creatables) > 0 { - insertCreatableIdxStmt, err = tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") - if err != nil { - return - } - defer insertCreatableIdxStmt.Close() - - deleteCreatableIdxStmt, err = tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") - if err != nil { - return - } - defer deleteCreatableIdxStmt.Close() - } - for addr, data := range updates { if data.new.IsZero() { // prune empty accounts @@ -661,15 +647,29 @@ func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creat } - for cidx, cdelta := range creatables { - if cdelta.created { - _, err = insertCreatableIdxStmt.Exec(cidx, cdelta.creator[:], cdelta.ctype) - } else { - _, err = deleteCreatableIdxStmt.Exec(cidx, cdelta.ctype) + if len(creatables) > 0 { + insertCreatableIdxStmt, err = tx.Prepare("INSERT INTO assetcreators (asset, creator, ctype) VALUES (?, ?, ?)") + if err != nil { + return } + defer insertCreatableIdxStmt.Close() + + deleteCreatableIdxStmt, err = tx.Prepare("DELETE FROM assetcreators WHERE asset=? AND ctype=?") if err != nil { return } + defer deleteCreatableIdxStmt.Close() + + for cidx, cdelta := range creatables { + if cdelta.created { + _, err = insertCreatableIdxStmt.Exec(cidx, cdelta.creator[:], cdelta.ctype) + } else { + _, err = deleteCreatableIdxStmt.Exec(cidx, cdelta.ctype) + } + if err != nil { + return + } + } } return From 4d85b5a2bab591328c8fb4b97c7eadbd9c02f09e Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 24 Jul 2020 16:49:07 -0400 Subject: [PATCH 160/267] Reformat some of the short commands descriptions. (#1298) Updates the short commands descriptions --- cmd/algocfg/main.go | 2 +- cmd/diagcfg/telemetry.go | 2 +- cmd/goal/clerk.go | 8 ++++---- cmd/goal/commands.go | 6 +++--- cmd/goal/completion.go | 6 +++--- cmd/goal/kmd.go | 4 ++-- cmd/goal/ledger.go | 2 +- cmd/goal/logging.go | 2 +- cmd/goal/network.go | 6 +++--- cmd/goal/node.go | 10 +++++----- cmd/goal/wallet.go | 6 +++--- cmd/netgoal/generate.go | 4 ++-- cmd/netgoal/network.go | 4 ++-- cmd/nodecfg/apply.go | 4 ++-- 14 files changed, 33 insertions(+), 33 deletions(-) diff --git a/cmd/algocfg/main.go b/cmd/algocfg/main.go index ce0398799d..045712ac8f 100644 --- a/cmd/algocfg/main.go +++ b/cmd/algocfg/main.go @@ -31,7 +31,7 @@ func init() { var rootCmd = &cobra.Command{ Use: "algocfg", - Short: "tool for inspecting and updating algod's config.json file", + Short: "Tool for inspecting and updating algod's config.json file", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { cmd.HelpFunc()(cmd, args) diff --git a/cmd/diagcfg/telemetry.go b/cmd/diagcfg/telemetry.go index bc832a46f4..85046dd3ae 100644 --- a/cmd/diagcfg/telemetry.go +++ b/cmd/diagcfg/telemetry.go @@ -169,7 +169,7 @@ var telemetryNameCmd = &cobra.Command{ var telemetryEndpointCmd = &cobra.Command{ Use: "endpoint -e ", - Short: "sets the \"URI\" property", + Short: "Sets the \"URI\" property", Long: `Sets the "URI" property in the telemetry configuration`, Run: func(cmd *cobra.Command, args []string) { cfg := readTelemetryConfigOrExit() diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index f7714f1027..9dd3105e5a 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -563,7 +563,7 @@ var rawsendCmd = &cobra.Command{ var inspectCmd = &cobra.Command{ Use: "inspect [input file 1] [input file 2]...", - Short: "print a transaction file", + Short: "Print a transaction file", Long: `Loads a transaction file, attempts to decode the transaction, and displays the decoded information.`, Run: func(cmd *cobra.Command, args []string) { for _, txFilename := range args { @@ -851,7 +851,7 @@ func disassembleFile(fname, outname string) { var compileCmd = &cobra.Command{ Use: "compile [input file 1] [input file 2]...", - Short: "compile a contract program", + Short: "Compile a contract program", Long: "Reads a TEAL contract program and compiles it to binary output and contract address.", Run: func(cmd *cobra.Command, args []string) { for _, fname := range args { @@ -909,7 +909,7 @@ var compileCmd = &cobra.Command{ var dryrunCmd = &cobra.Command{ Use: "dryrun", - Short: "test a program offline", + Short: "Test a program offline", Long: "Test a TEAL program offline under various conditions and verbosity.", Run: func(cmd *cobra.Command, args []string) { data, err := readFile(txFilename) @@ -972,7 +972,7 @@ var dryrunCmd = &cobra.Command{ var dryrunRemoteCmd = &cobra.Command{ Use: "dryrun-remote", - Short: "test a program with algod's dryrun REST endpoint", + Short: "Test a program with algod's dryrun REST endpoint", Long: "Test a TEAL program with algod's dryrun REST endpoint under various conditions and verbosity.", Run: func(cmd *cobra.Command, args []string) { data, err := readFile(txFilename) diff --git a/cmd/goal/commands.go b/cmd/goal/commands.go index b572eece0d..d458beef45 100644 --- a/cmd/goal/commands.go +++ b/cmd/goal/commands.go @@ -103,7 +103,7 @@ func init() { var rootCmd = &cobra.Command{ Use: "goal", - Short: "CLI for interacting with Algorand.", + Short: "CLI for interacting with Algorand", Long: `GOAL is the CLI for interacting Algorand software instance. The binary 'goal' is installed alongside the algod binary and is considered an integral part of the complete installation. The binaries should be used in tandem - you should not try to use a version of goal with a different version of algod.`, Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { @@ -160,7 +160,7 @@ func main() { var versionCmd = &cobra.Command{ Use: "version", - Short: "The current version of the Algorand daemon (algod).", + Short: "The current version of the Algorand daemon (algod)", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { onDataDirs(func(dataDir string) { @@ -184,7 +184,7 @@ var versionCmd = &cobra.Command{ var licenseCmd = &cobra.Command{ Use: "license", - Short: "Display license information.", + Short: "Display license information", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { fmt.Println(config.GetLicenseInfo()) diff --git a/cmd/goal/completion.go b/cmd/goal/completion.go index 9a27c3c5e9..5c5ddb0eea 100644 --- a/cmd/goal/completion.go +++ b/cmd/goal/completion.go @@ -29,7 +29,7 @@ func init() { var completionCmd = &cobra.Command{ Use: "completion", - Short: "Shell completion helper.", + Short: "Shell completion helper", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { // If no arguments passed, we should fallback to help @@ -39,7 +39,7 @@ var completionCmd = &cobra.Command{ var bashCompletionCmd = &cobra.Command{ Use: "bash", - Short: "Generate bash completion commands.", + Short: "Generate bash completion commands", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { rootCmd.GenBashCompletion(os.Stdout) @@ -48,7 +48,7 @@ var bashCompletionCmd = &cobra.Command{ var zshCompletionCmd = &cobra.Command{ Use: "zsh", - Short: "Generate zsh completion commands.", + Short: "Generate zsh completion commands", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { rootCmd.GenZshCompletion(os.Stdout) diff --git a/cmd/goal/kmd.go b/cmd/goal/kmd.go index 5e24813e38..07730a0585 100644 --- a/cmd/goal/kmd.go +++ b/cmd/goal/kmd.go @@ -61,7 +61,7 @@ func startKMDForDataDir(binDir, algodDataDir, kmdDataDir string) { var startKMDCmd = &cobra.Command{ Use: "start", - Short: "Start the kmd process, or restart it with an updated timeout.", + Short: "Start the kmd process, or restart it with an updated timeout", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { binDir, err := util.ExeDir() @@ -78,7 +78,7 @@ var startKMDCmd = &cobra.Command{ var stopKMDCmd = &cobra.Command{ Use: "stop", - Short: "Stop the kmd process if it is running.", + Short: "Stop the kmd process if it is running", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { binDir, err := util.ExeDir() diff --git a/cmd/goal/ledger.go b/cmd/goal/ledger.go index 4e7e49ea61..7534d1193b 100644 --- a/cmd/goal/ledger.go +++ b/cmd/goal/ledger.go @@ -45,7 +45,7 @@ func init() { var ledgerCmd = &cobra.Command{ Use: "ledger", - Short: "Access ledger-related details.", + Short: "Access ledger-related details", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { // If no arguments passed, we should fallback to help diff --git a/cmd/goal/logging.go b/cmd/goal/logging.go index 1f5b6ed9c2..22d72466bb 100644 --- a/cmd/goal/logging.go +++ b/cmd/goal/logging.go @@ -90,7 +90,7 @@ var enableCmd = &cobra.Command{ var disableCmd = &cobra.Command{ Use: "disable", - Short: "Disable Algorand remote logging.", + Short: "Disable Algorand remote logging", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { fmt.Fprintf(os.Stderr, "Warning: `goal logging disable` deprecated, use `diagcfg telemetry disable`\n") diff --git a/cmd/goal/network.go b/cmd/goal/network.go index e354d0bb51..fc50da86b0 100644 --- a/cmd/goal/network.go +++ b/cmd/goal/network.go @@ -156,7 +156,7 @@ var networkStartCmd = &cobra.Command{ var networkRestartCmd = &cobra.Command{ Use: "restart", - Short: "Restart a deployed private network.", + Short: "Restart a deployed private network", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { network, binDir := getNetworkAndBinDir() @@ -171,7 +171,7 @@ var networkRestartCmd = &cobra.Command{ var networkStopCmd = &cobra.Command{ Use: "stop", - Short: "Stop a deployed private network.", + Short: "Stop a deployed private network", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { network, binDir := getNetworkAndBinDir() @@ -182,7 +182,7 @@ var networkStopCmd = &cobra.Command{ var networkStatusCmd = &cobra.Command{ Use: "status", - Short: "Prints status for all nodes in a deployed private network.", + Short: "Prints status for all nodes in a deployed private network", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { network, binDir := getNetworkAndBinDir() diff --git a/cmd/goal/node.go b/cmd/goal/node.go index 0b5a139641..2ae52640a6 100644 --- a/cmd/goal/node.go +++ b/cmd/goal/node.go @@ -153,7 +153,7 @@ func catchpointCmdArgument(cmd *cobra.Command, args []string) error { var startCmd = &cobra.Command{ Use: "start", - Short: "Initialize the specified Algorand node.", + Short: "Initialize the specified Algorand node", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { if !verifyPeerDialArg() { @@ -228,7 +228,7 @@ func getRunHostedConfigFlag(dataDir string) bool { var stopCmd = &cobra.Command{ Use: "stop", - Short: "stop the specified Algorand node.", + Short: "Stop the specified Algorand node", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { binDir, err := util.ExeDir() @@ -256,7 +256,7 @@ var stopCmd = &cobra.Command{ var restartCmd = &cobra.Command{ Use: "restart", - Short: "Stop, and then start, the specified Algorand node.", + Short: "Stop, and then start, the specified Algorand node", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { if !verifyPeerDialArg() { @@ -319,7 +319,7 @@ var restartCmd = &cobra.Command{ var generateTokenCmd = &cobra.Command{ Use: "generatetoken", - Short: "Generate and install a new API token.", + Short: "Generate and install a new API token", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { onDataDirs(func(dataDir string) { @@ -553,7 +553,7 @@ func isValidIP(userInput string) bool { var createCmd = &cobra.Command{ Use: "create", - Short: "Create a node at the desired data directory for the desired network.", + Short: "Create a node at the desired data directory for the desired network", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { diff --git a/cmd/goal/wallet.go b/cmd/goal/wallet.go index 756213d98c..76d68157fe 100644 --- a/cmd/goal/wallet.go +++ b/cmd/goal/wallet.go @@ -48,7 +48,7 @@ func init() { var walletCmd = &cobra.Command{ Use: "wallet", - Short: "Manage wallets: encrypted collections of Algorand account keys.", + Short: "Manage wallets: encrypted collections of Algorand account keys", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, args []string) { // Update the default wallet @@ -79,7 +79,7 @@ var walletCmd = &cobra.Command{ var newWalletCmd = &cobra.Command{ Use: "new [wallet name]", - Short: "Create a new wallet.", + Short: "Create a new wallet", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { var err error @@ -185,7 +185,7 @@ var newWalletCmd = &cobra.Command{ var listWalletsCmd = &cobra.Command{ Use: "list", - Short: "List wallets managed by kmd.", + Short: "List wallets managed by kmd", Args: validateNoPosArgsFn, Run: func(cmd *cobra.Command, _ []string) { onDataDirs(func(dataDir string) { diff --git a/cmd/netgoal/generate.go b/cmd/netgoal/generate.go index 5f845dfd4b..3cfee4fdc7 100644 --- a/cmd/netgoal/generate.go +++ b/cmd/netgoal/generate.go @@ -79,8 +79,8 @@ var generateTemplateLines = []string{ var generateCmd = &cobra.Command{ Use: "generate", - Short: "generate network template", - Long: `generate network template or genesis.json + Short: "Generate network template", + Long: `Generate network template or genesis.json -r is required for all netgoal commands but unused by generate template modes for -t:`, diff --git a/cmd/netgoal/network.go b/cmd/netgoal/network.go index a4fd89624d..f6059aa032 100644 --- a/cmd/netgoal/network.go +++ b/cmd/netgoal/network.go @@ -55,8 +55,8 @@ func init() { var networkBuildCmd = &cobra.Command{ Use: "build", - Short: "build network deployment artifacts", - Long: `build network deployment artifacts for modifying before deploying`, + Short: "Build network deployment artifacts", + Long: `Build network deployment artifacts for modifying before deploying`, Run: func(cmd *cobra.Command, args []string) { // Similar to `goal network create`, we need to generate a genesis.json and wallets and store somewhere. // We have a lot more parameters to define, so we'll support a subset of parameters on cmdline but diff --git a/cmd/nodecfg/apply.go b/cmd/nodecfg/apply.go index c794fdff68..794a27b498 100644 --- a/cmd/nodecfg/apply.go +++ b/cmd/nodecfg/apply.go @@ -54,8 +54,8 @@ func init() { var applyCmd = &cobra.Command{ Use: "apply", - Short: "apply a node configuration to a new host", - Long: `apply a node configuration to a new host providing feedback on progress`, + Short: "Apply a node configuration to a new host", + Long: `Apply a node configuration to a new host providing feedback on progress`, Run: func(cmd *cobra.Command, args []string) { if err := validateArgs(); err != nil { reportErrorf("Error validating arguments: %v", err) From e1716ac5f56807a94313ced12220d53c33211fb1 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Fri, 24 Jul 2020 20:15:48 -0400 Subject: [PATCH 161/267] -- convert to append-auth-addr --- cmd/algokey/multisig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/algokey/multisig.go b/cmd/algokey/multisig.go index fed16636dd..7335d19948 100644 --- a/cmd/algokey/multisig.go +++ b/cmd/algokey/multisig.go @@ -50,7 +50,7 @@ func init() { multisigCmd.Flags().StringVarP(&multisigOutfile, "outfile", "o", "", "Transaction output filename") multisigCmd.MarkFlagRequired("outfile") - appendAuthAddrCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig pre image parameters - [threshold] [Address 1] [Address 2]") + appendAuthAddrCmd.Flags().StringVarP(&msigParams, "params", "p", "", "Multisig pre image parameters - [threshold] [Address 1] [Address 2] ...") appendAuthAddrCmd.MarkFlagRequired("params") appendAuthAddrCmd.Flags().StringVarP(&multisigTxfile, "txfile", "t", "", "Transaction input filename") appendAuthAddrCmd.MarkFlagRequired("txfile") From 208ec12f4a6dcdf9f5262817acc97d866ac222c1 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy <65323360+algorandskiy@users.noreply.github.com> Date: Fri, 24 Jul 2020 20:36:01 -0400 Subject: [PATCH 162/267] TEAL: assembler error on branching behind last instruction (#1295) * TEAL v1 does not allow such program, so make assembler compatible * TEAL v2 permits the mentioned behavior --- data/transactions/logic/assembler.go | 34 +++++++++++---- data/transactions/logic/assembler_test.go | 35 +++++++++++++++- .../transactions/logic/backwardCompat_test.go | 41 +++++++++++++++++++ data/transactions/logic/eval_test.go | 7 +++- 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 9dca82bf63..e6c01f6821 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -1072,6 +1072,16 @@ func (ops *OpStream) assemble(fin io.Reader) error { err := fmt.Errorf("unknown opcode %v", opstring) return lineErr(ops.sourceLine, err) } + + // backward compatibility: do not allow jumps behind last instruction in TEAL v1 + if ops.Version <= 1 { + for label, dest := range ops.labels { + if dest == ops.Out.Len() { + return fmt.Errorf(":%d label %v is too far away", ops.sourceLine, label) + } + } + } + // TODO: warn if expected resulting stack is not len==1 ? return ops.resolveLabels() } @@ -1298,6 +1308,13 @@ func (dis *disassembleState) putLabel(label string, target int) { dis.pendingLabels[target] = label } +func (dis *disassembleState) outputLabelIfNeeded() (err error) { + if label, hasLabel := dis.pendingLabels[dis.pc]; hasLabel { + _, err = fmt.Fprintf(dis.out, "%s:\n", label) + } + return +} + type disassembleFunc func(dis *disassembleState, spec *OpSpec) func disDefault(dis *disassembleState, spec *OpSpec) { @@ -1684,7 +1701,7 @@ type disInfo struct { hasStatefulOps bool } -// DisassembleInstrumented is like Disassemble, but additionally returns where +// disassembleInstrumented is like Disassemble, but additionally returns where // each program counter value maps in the disassembly func disassembleInstrumented(program []byte) (text string, ds disInfo, err error) { out := strings.Builder{} @@ -1703,13 +1720,9 @@ func disassembleInstrumented(program []byte) (text string, ds disInfo, err error fmt.Fprintf(dis.out, "// version %d\n", version) dis.pc = vlen for dis.pc < len(program) { - label, hasLabel := dis.pendingLabels[dis.pc] - if hasLabel { - _, dis.err = fmt.Fprintf(dis.out, "%s:\n", label) - if dis.err != nil { - err = dis.err - return - } + err = dis.outputLabelIfNeeded() + if err != nil { + return } op := opsByOpcode[version][program[dis.pc]] if op.Modes == runModeApplication { @@ -1736,6 +1749,11 @@ func disassembleInstrumented(program []byte) (text string, ds disInfo, err error } dis.pc = dis.nextpc } + err = dis.outputLabelIfNeeded() + if err != nil { + return + } + text = out.String() return } diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 1610443194..445df23f50 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -789,6 +789,7 @@ bnz nowhere` } func TestAssembleJumpToTheEnd(t *testing.T) { + t.Parallel() text := `intcblock 1 intc 0 intc 0 @@ -931,6 +932,8 @@ func TestAssembleDisassembleCycle(t *testing.T) { } func TestAssembleDisassembleErrors(t *testing.T) { + t.Parallel() + source := `txn Sender` program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1040,6 +1043,7 @@ func TestAssembleDisassembleErrors(t *testing.T) { } func TestAssembleVersions(t *testing.T) { + t.Parallel() text := `int 1 txna Accounts 0 ` @@ -1067,6 +1071,7 @@ int 1 } func TestAssembleAsset(t *testing.T) { + t.Parallel() source := "int 0\nint 0\nasset_holding_get ABC 1" _, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) @@ -1089,6 +1094,7 @@ func TestAssembleAsset(t *testing.T) { } func TestDisassembleSingleOp(t *testing.T) { + t.Parallel() // test ensures no double arg_0 entries in disassembly listing sample := "// version 2\narg_0\n" program, err := AssembleStringWithVersion(sample, AssemblerMaxVersion) @@ -1100,6 +1106,7 @@ func TestDisassembleSingleOp(t *testing.T) { } func TestDisassembleTxna(t *testing.T) { + t.Parallel() // check txn and txna are properly disassembled txnSample := "// version 2\ntxn Sender\n" program, err := AssembleStringWithVersion(txnSample, AssemblerMaxVersion) @@ -1125,6 +1132,7 @@ func TestDisassembleTxna(t *testing.T) { } func TestDisassembleGtxna(t *testing.T) { + t.Parallel() // check gtxn and gtxna are properly disassembled gtxnSample := "// version 2\ngtxn 0 Sender\n" program, err := AssembleStringWithVersion(gtxnSample, AssemblerMaxVersion) @@ -1149,7 +1157,29 @@ func TestDisassembleGtxna(t *testing.T) { require.Equal(t, gtxnaSample, disassembled) } +func TestDisassembleLastLabel(t *testing.T) { + t.Parallel() + + // starting from TEAL v2 branching to the last line are legal + for v := uint64(2); v <= AssemblerMaxVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + source := fmt.Sprintf(`// version %d +intcblock 1 +intc_0 +bnz label1 +label1: +`, v) + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + dis, err := Disassemble(program) + require.NoError(t, err) + require.Equal(t, source, dis) + }) + } +} + func TestAssembleOffsets(t *testing.T) { + t.Parallel() source := "err" program, offsets, err := AssembleStringWithVersionEx(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1247,6 +1277,7 @@ err } func TestHasStatefulOps(t *testing.T) { + t.Parallel() source := "int 1" program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) @@ -1267,6 +1298,7 @@ err } func TestStringLiteralParsing(t *testing.T) { + t.Parallel() s := `"test"` e := []byte(`test`) result, err := parseStringLiteral(s) @@ -1343,6 +1375,7 @@ func TestStringLiteralParsing(t *testing.T) { } func TestPragmaStream(t *testing.T) { + t.Parallel() for v := uint64(1); v <= AssemblerMaxVersion; v++ { text := fmt.Sprintf("#pragma version %d", v) sr := strings.NewReader(text) @@ -1452,6 +1485,7 @@ func TestPragmaStream(t *testing.T) { } func TestAssemblePragmaVersion(t *testing.T) { + t.Parallel() text := `#pragma version 1 int 1 ` @@ -1513,7 +1547,6 @@ len func TestAssembleConstants(t *testing.T) { t.Parallel() - for v := uint64(1); v <= AssemblerMaxVersion; v++ { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { _, err := AssembleStringWithVersion("intc 1", v) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index daa8242e22..d745dd746e 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -250,6 +250,7 @@ dup var programTEALv1 = "01200500010220ffffffffffffffffff012608014120559aead08264d5795d3909718cdd05abd49572e84fe55590eef31a88a08fdffd0142201f675bff07515f5df96737194ea945c36c41e7b4fcef307b7cd4d0e602a6911101432034b99f8dde1ba273c0a28cf5b2e4dbe497f8cb2453de0c8ba6d578c9431a62cb0100200000000000000000000000000000000000000000000000000000000000000000280129122a022b1210270403270512102d2e2f041022082209230a230b240c220d230e230f231022112312231314301525121617182319231a221b21041c1d12222312242512102104231210482829122a2b121027042706121048310031071331013102121022310413103105310613103108311613103109310a1210310b310f1310310c310d1210310e31101310311131121310311331141210311531171210483300003300071333000133000212102233000413103300053300061310330008330016131033000933000a121033000b33000f131033000c33000d121033000e3300101310330011330012131033001333001412103300153300171210483200320112320232041310320327071210350034001040000100234912" func TestBackwardCompatTEALv1(t *testing.T) { + t.Parallel() var s crypto.Seed crypto.RandBytes(s[:]) c := crypto.GenerateSignatureSecrets(s) @@ -330,6 +331,7 @@ func TestBackwardCompatTEALv1(t *testing.T) { // ensure v2 fields error on pre TEAL v2 logicsig version // ensure v2 fields error in v1 program func TestBackwardCompatGlobalFields(t *testing.T) { + t.Parallel() var fields []string for _, fs := range globalFieldSpecs { if fs.version > 1 { @@ -395,6 +397,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { // ensure v2 fields error in v1 program func TestBackwardCompatTxnFields(t *testing.T) { + t.Parallel() var fields []txnFieldSpec for _, fs := range txnFieldSpecs { if fs.version > 1 { @@ -480,3 +483,41 @@ func TestBackwardCompatTxnFields(t *testing.T) { } } } + +func TestBackwardCompatAssemble(t *testing.T) { + // TEAL v1 does not allow branching to the last line + // TEAL v2 makes such programs legal + t.Parallel() + source := `int 0 +int 1 +bnz done +done:` + + t.Run("v=default", func(t *testing.T) { + _, err := AssembleString(source) + require.Error(t, err) + require.Contains(t, err.Error(), ":4 label done is too far away") + }) + + t.Run("v=0", func(t *testing.T) { + _, err := AssembleStringWithVersion(source, 0) + require.Error(t, err) + require.Contains(t, err.Error(), ":4 label done is too far away") + }) + + t.Run("v=1", func(t *testing.T) { + _, err := AssembleStringWithVersion(source, 1) + require.Error(t, err) + require.Contains(t, err.Error(), ":4 label done is too far away") + }) + + for v := uint64(2); v <= AssemblerMaxVersion; v++ { + t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + ep := defaultEvalParams(nil, nil) + _, err = Eval(program, ep) + require.NoError(t, err) + }) + } +} diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 6e362c83d5..314f4318d9 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -2662,9 +2662,12 @@ func TestShortProgram(t *testing.T) { t.Run(fmt.Sprintf("v=%d", v), func(t *testing.T) { program, err := AssembleStringWithVersion(`int 1 bnz done -done:`, v) +done: +int 1 +`, v) require.NoError(t, err) - program = program[:len(program)-1] + // cut two last bytes - intc_1 and last byte of bnz + program = program[:len(program)-2] cost, err := Check(program, defaultEvalParams(nil, nil)) require.Error(t, err) require.True(t, cost < 1000) From 9cbbf9e28c91cc3698fbf832b3e2374a520c688f Mon Sep 17 00:00:00 2001 From: algomaxj <65551122+algomaxj@users.noreply.github.com> Date: Fri, 24 Jul 2020 21:35:31 -0400 Subject: [PATCH 163/267] base64 encode binary keys/values in json `TealKeyValue`, `EvalDeltaKeyValue` model (#1293) Byte keys and values should be base64 encoded when represented as json EvalDeltaKeyValue values were being base64 encoded, but their keys weren' --- cmd/tealdbg/dryrunRequest.go | 6 ++- daemon/algod/api/server/v2/account.go | 49 ++++++++++++++++------ daemon/algod/api/server/v2/account_test.go | 6 +-- daemon/algod/api/server/v2/dryrun.go | 13 ++++-- daemon/algod/api/server/v2/dryrun_test.go | 45 +++++++++++--------- 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/cmd/tealdbg/dryrunRequest.go b/cmd/tealdbg/dryrunRequest.go index e8c05e1f12..fbaf22b56e 100644 --- a/cmd/tealdbg/dryrunRequest.go +++ b/cmd/tealdbg/dryrunRequest.go @@ -80,7 +80,11 @@ func balanceRecordsFromDdr(ddr *v2.DryrunRequest) (records []basics.BalanceRecor return } // deserialize app params and update account data - params := v2.ApplicationParamsToAppParams(&a.Params) + var params basics.AppParams + params, err = v2.ApplicationParamsToAppParams(&a.Params) + if err != nil { + return + } appIdx := basics.AppIndex(a.Id) ad := accounts[addr] if ad.AppParams == nil { diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index c0b94b5189..190e8a404e 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -17,6 +17,7 @@ package v2 import ( + "encoding/base64" "errors" "github.com/algorand/go-algorand/crypto" @@ -120,10 +121,10 @@ func convertTKVToGenerated(tkv *basics.TealKeyValue) *generated.TealKeyValueStor var converted generated.TealKeyValueStore for k, v := range *tkv { converted = append(converted, generated.TealKeyValue{ - Key: k, + Key: base64.StdEncoding.EncodeToString([]byte(k)), Value: generated.TealValue{ Type: uint64(v.Type), - Bytes: v.Bytes, + Bytes: base64.StdEncoding.EncodeToString([]byte(v.Bytes)), Uint: v.Uint, }, }) @@ -131,20 +132,32 @@ func convertTKVToGenerated(tkv *basics.TealKeyValue) *generated.TealKeyValueStor return &converted } -func convertGeneratedTKV(akvs *generated.TealKeyValueStore) basics.TealKeyValue { +func convertGeneratedTKV(akvs *generated.TealKeyValueStore) (basics.TealKeyValue, error) { if akvs == nil || len(*akvs) == 0 { - return nil + return nil, nil } tkv := make(basics.TealKeyValue) for _, kv := range *akvs { - tkv[kv.Key] = basics.TealValue{ + // Decode base-64 encoded map key + decodedKey, err := base64.StdEncoding.DecodeString(kv.Key) + if err != nil { + return nil, err + } + + // Decode base-64 encoded map value (OK even if empty string) + decodedBytes, err := base64.StdEncoding.DecodeString(kv.Value.Bytes) + if err != nil { + return nil, err + } + + tkv[string(decodedKey)] = basics.TealValue{ Type: basics.TealType(kv.Value.Type), Uint: kv.Value.Uint, - Bytes: kv.Value.Bytes, + Bytes: string(decodedBytes), } } - return tkv + return tkv, nil } // AccountToAccountData converts v2.generated.Account to basics.AccountData @@ -220,12 +233,16 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { if a.AppsLocalState != nil && len(*a.AppsLocalState) > 0 { appLocalStates = make(map[basics.AppIndex]basics.AppLocalState, len(*a.AppsLocalState)) for _, ls := range *a.AppsLocalState { + kv, err := convertGeneratedTKV(ls.KeyValue) + if err != nil { + return basics.AccountData{}, err + } appLocalStates[basics.AppIndex(ls.Id)] = basics.AppLocalState{ Schema: basics.StateSchema{ NumUint: ls.Schema.NumUint, NumByteSlice: ls.Schema.NumByteSlice, }, - KeyValue: convertGeneratedTKV(ls.KeyValue), + KeyValue: kv, } } } @@ -234,7 +251,11 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { if a.CreatedApps != nil && len(*a.CreatedApps) > 0 { appParams = make(map[basics.AppIndex]basics.AppParams, len(*a.CreatedApps)) for _, params := range *a.CreatedApps { - appParams[basics.AppIndex(params.Id)] = ApplicationParamsToAppParams(¶ms.Params) + ap, err := ApplicationParamsToAppParams(¶ms.Params) + if err != nil { + return basics.AccountData{}, err + } + appParams[basics.AppIndex(params.Id)] = ap } } @@ -289,7 +310,7 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { } // ApplicationParamsToAppParams converts generated.ApplicationParams to basics.AppParams -func ApplicationParamsToAppParams(gap *generated.ApplicationParams) basics.AppParams { +func ApplicationParamsToAppParams(gap *generated.ApplicationParams) (basics.AppParams, error) { ap := basics.AppParams{ ApprovalProgram: gap.ApprovalProgram, ClearStateProgram: gap.ClearStateProgram, @@ -306,9 +327,13 @@ func ApplicationParamsToAppParams(gap *generated.ApplicationParams) basics.AppPa NumByteSlice: gap.GlobalStateSchema.NumByteSlice, } } - ap.GlobalState = convertGeneratedTKV(gap.GlobalState) + kv, err := convertGeneratedTKV(gap.GlobalState) + if err != nil { + return basics.AppParams{}, err + } + ap.GlobalState = kv - return ap + return ap, nil } // AppParamsToApplication converts basics.AppParams to generated.Application diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index aed1147201..38fab7f805 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -78,17 +78,17 @@ func TestAccount(t *testing.T) { require.Equal(t, uint64(0), ls.Schema.NumByteSlice) require.Equal(t, 2, len(*ls.KeyValue)) value1 := generated.TealKeyValue{ - Key: "uint", + Key: b64("uint"), Value: generated.TealValue{ Type: uint64(basics.TealUintType), Uint: 2, }, } value2 := generated.TealKeyValue{ - Key: "bytes", + Key: b64("bytes"), Value: generated.TealValue{ Type: uint64(basics.TealBytesType), - Bytes: "value", + Bytes: b64("value"), }, } require.Contains(t, *ls.KeyValue, value1) diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index c7e78854b5..eee3d19dab 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -289,7 +289,10 @@ func (dl *dryrunLedger) Get(addr basics.Address, withPendingRewards bool) (basic if ok { any = true app := dl.dr.Apps[appi] - params := ApplicationParamsToAppParams(&app.Params) + params, err := ApplicationParamsToAppParams(&app.Params) + if err != nil { + return basics.BalanceRecord{}, err + } if out.AppParams == nil { out.AppParams = make(map[basics.AppIndex]basics.AppParams) out.AppParams[basics.AppIndex(app.Id)] = params @@ -447,7 +450,11 @@ func doDryrunRequest(dr *DryrunRequest, proto *config.ConsensusParams, response ok := false for _, appt := range dr.Apps { if appt.Id == uint64(appIdx) { - app = ApplicationParamsToAppParams(&appt.Params) + app, err = ApplicationParamsToAppParams(&appt.Params) + if err != nil { + response.Error = err.Error() + return + } ok = true break } @@ -522,7 +529,7 @@ func StateDeltaToStateDelta(sd basics.StateDelta) *generated.StateDelta { } // basics.DeleteAction does not require Uint/Bytes kv := generated.EvalDeltaKeyValue{ - Key: k, + Key: base64.StdEncoding.EncodeToString([]byte(k)), Value: value, } gsd = append(gsd, kv) diff --git a/daemon/algod/api/server/v2/dryrun_test.go b/daemon/algod/api/server/v2/dryrun_test.go index 1597c96187..c576a8518d 100644 --- a/daemon/algod/api/server/v2/dryrun_test.go +++ b/daemon/algod/api/server/v2/dryrun_test.go @@ -60,6 +60,10 @@ func dbStack(stack []generated.TealValue) string { return strings.Join(parts, " ") } +func b64(s string) string { + return base64.StdEncoding.EncodeToString([]byte(s)) +} + func logTrace(t *testing.T, lines []string, trace []generated.DryrunState) { var disasm string for _, ds := range trace { @@ -396,8 +400,8 @@ func TestDryrunGlobal1(t *testing.T) { } gkv := generated.TealKeyValueStore{ generated.TealKeyValue{ - Key: "foo", - Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + Key: b64("foo"), + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: b64("bar")}, }, } dr.Apps = []generated.Application{ @@ -445,8 +449,8 @@ func TestDryrunGlobal2(t *testing.T) { } gkv := generated.TealKeyValueStore{ generated.TealKeyValue{ - Key: "foo", - Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + Key: b64("foo"), + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: b64("bar")}, }, } dr.Apps = []generated.Application{ @@ -525,10 +529,10 @@ func TestDryrunLocal1(t *testing.T) { if lds.Address == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" { addrFound = true for _, ld := range lds.Delta { - if ld.Key == "foo" { + if ld.Key == b64("foo") { valueFound = true assert.Equal(t, ld.Value.Action, uint64(basics.SetBytesAction)) - assert.Equal(t, *ld.Value.Bytes, "YmFy") // bar + assert.Equal(t, *ld.Value.Bytes, b64("bar")) } } @@ -603,10 +607,10 @@ func TestDryrunLocal1A(t *testing.T) { if lds.Address == "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ" { addrFound = true for _, ld := range lds.Delta { - if ld.Key == "foo" { + if ld.Key == b64("foo") { valueFound = true assert.Equal(t, ld.Value.Action, uint64(basics.SetBytesAction)) - assert.Equal(t, *ld.Value.Bytes, "YmFy") // bar + assert.Equal(t, *ld.Value.Bytes, b64("bar")) } } @@ -659,8 +663,11 @@ func TestDryrunLocalCheck(t *testing.T) { } localv := make(generated.TealKeyValueStore, 1) localv[0] = generated.TealKeyValue{ - Key: "foo", - Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + Key: b64("foo"), + Value: generated.TealValue{ + Type: uint64(basics.TealBytesType), + Bytes: b64("bar"), + }, } dr.Accounts = []generated.Account{ @@ -711,8 +718,8 @@ func TestDryrunEncodeDecode(t *testing.T) { } localv := make(generated.TealKeyValueStore, 1) localv[0] = generated.TealKeyValue{ - Key: "foo", - Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: "bar"}, + Key: b64("foo"), + Value: generated.TealValue{Type: uint64(basics.TealBytesType), Bytes: b64("bar")}, } gdr.Accounts = []generated.Account{ @@ -942,17 +949,17 @@ func TestStateDeltaToStateDelta(t *testing.T) { var keys []string // test with a loop because sd is a map and iteration order is random for _, item := range *gsd { - if item.Key == "byteskey" { + if item.Key == b64("byteskey") { require.Equal(t, uint64(1), item.Value.Action) require.Nil(t, item.Value.Uint) require.NotNil(t, item.Value.Bytes) - require.Equal(t, base64.StdEncoding.EncodeToString([]byte("test")), *item.Value.Bytes) - } else if item.Key == "intkey" { + require.Equal(t, b64("test"), *item.Value.Bytes) + } else if item.Key == b64("intkey") { require.Equal(t, uint64(2), item.Value.Action) require.NotNil(t, item.Value.Uint) require.Equal(t, uint64(11), *item.Value.Uint) require.Nil(t, item.Value.Bytes) - } else if item.Key == "delkey" { + } else if item.Key == b64("delkey") { require.Equal(t, uint64(3), item.Value.Action) require.Nil(t, item.Value.Uint) require.Nil(t, item.Value.Bytes) @@ -960,7 +967,7 @@ func TestStateDeltaToStateDelta(t *testing.T) { keys = append(keys, item.Key) } require.Equal(t, 3, len(keys)) - require.Contains(t, keys, "intkey") - require.Contains(t, keys, "byteskey") - require.Contains(t, keys, "delkey") + require.Contains(t, keys, b64("intkey")) + require.Contains(t, keys, b64("byteskey")) + require.Contains(t, keys, b64("delkey")) } From 79fca178351e8d7e80668f656a9aa87ae9e5afa1 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 25 Jul 2020 15:24:31 -0400 Subject: [PATCH 164/267] update code comments. --- ledger/acctupdates.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index d079b1bb35..594a8b0259 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -255,6 +255,7 @@ func (au *accountUpdates) waitAccountsWriting() { au.accountsWriting.Wait() } +// close closes the accountUpdates, waiting for all the child go-routine to complete func (au *accountUpdates) close() { if au.ctxCancel != nil { au.ctxCancel() @@ -357,7 +358,7 @@ func (au *accountUpdates) listCreatables(maxCreatableIdx basics.CreatableIndex, return res, nil } -// getLastCatchpointLabel retrieves the last catchpoint label that was stored to the database. +// GetLastCatchpointLabel retrieves the last catchpoint label that was stored to the database. func (au *accountUpdates) GetLastCatchpointLabel() string { au.accountsMu.RLock() defer au.accountsMu.RUnlock() @@ -495,6 +496,7 @@ func (au *accountUpdates) Totals(rnd basics.Round) (totals AccountTotals, err er return au.totalsImpl(rnd) } +// GetCatchpointStream returns an io.Reader to the catchpoint file associated with the provided round func (au *accountUpdates) GetCatchpointStream(round basics.Round) (io.ReadCloser, error) { dbFileName := "" err := au.dbs.rdb.Atomic(func(ctx context.Context, tx *sql.Tx) (err error) { @@ -589,12 +591,13 @@ func (aul *accountUpdatesLedgerEvaluator) Totals(rnd basics.Round) (AccountTotal return aul.au.totalsImpl(rnd) } +// isDup return whether a transaction is a duplicate one. It's not needed by the accountUpdatesLedgerEvaluator and implemeted as a stub. func (aul *accountUpdatesLedgerEvaluator) isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error) { // this is a non-issue since this call will never be made on non-validating evaluation return false, fmt.Errorf("accountUpdatesLedgerEvaluator: tried to check for dup during accountUpdates initilization ") } -// Lookup returns the account balance for a given address at a given round, without the reward +// LookupWithoutRewards returns the account balance for a given address at a given round, without the reward func (aul *accountUpdatesLedgerEvaluator) LookupWithoutRewards(rnd basics.Round, addr basics.Address) (basics.AccountData, error) { return aul.au.lookupImpl(rnd, addr, false) } @@ -604,6 +607,7 @@ func (aul *accountUpdatesLedgerEvaluator) GetCreatorForRound(rnd basics.Round, c return aul.au.getCreatorForRoundImpl(rnd, cidx, ctype) } +// totalsImpl returns the totals for a given round func (au *accountUpdates) totalsImpl(rnd basics.Round) (totals AccountTotals, err error) { offset, err := au.roundOffset(rnd) if err != nil { @@ -748,7 +752,7 @@ func accountHashBuilder(addr basics.Address, accountData basics.AccountData, enc return hash[:] } -// Initialize accounts DB if needed and return account round +// accountsInitialize initializes the accounts DB if needed and return currrent account round. // as part of the initialization, it tests the current database schema version, and perform upgrade // procedures to bring it up to the database schema supported by the binary. func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (basics.Round, error) { @@ -1059,6 +1063,8 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basic return } +// newBlockImpl is the accountUpdates implementation of the ledgerTracker interface. This is the "internal" facing function +// which assumes that no lock need to be taken. func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) { proto := config.Consensus[blk.CurrentProtocol] rnd := blk.Round() @@ -1112,6 +1118,9 @@ func (au *accountUpdates) newBlockImpl(blk bookkeeping.Block, delta StateDelta) au.roundTotals = append(au.roundTotals, newTotals) } +// lookupImpl returns the accound data for a given address at a given round. The withRewards indicates whether the +// rewards should be added to the AccountData before returning. Note that the function doesn't update the account with the rewards, +// even while it could return the AccoutData which represent the "rewarded" account data. func (au *accountUpdates) lookupImpl(rnd basics.Round, addr basics.Address, withRewards bool) (data basics.AccountData, err error) { offset, err := au.roundOffset(rnd) if err != nil { @@ -1190,6 +1199,7 @@ func (au *accountUpdates) getCreatorForRoundImpl(rnd basics.Round, cidx basics.C return au.accountsq.lookupCreator(cidx, ctype) } +// accountsCreateCatchpointLabel creates a catchpoint label and write it. func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Round, totals AccountTotals, ledgerBlockDigest crypto.Digest, trieBalancesHash crypto.Digest) (label string, err error) { cpLabel := makeCatchpointLabel(committedRound, ledgerBlockDigest, trieBalancesHash, totals) label = cpLabel.String() @@ -1197,6 +1207,7 @@ func (au *accountUpdates) accountsCreateCatchpointLabel(committedRound basics.Ro return } +// roundOffset calculates the offset of the given round compared to the current dbRound. Requires that the lock would be taken. func (au *accountUpdates) roundOffset(rnd basics.Round) (offset uint64, err error) { if rnd < au.dbRound { err = fmt.Errorf("round %d before dbRound %d", rnd, au.dbRound) @@ -1212,6 +1223,8 @@ func (au *accountUpdates) roundOffset(rnd basics.Round) (offset uint64, err erro return off, nil } +// commitSyncer is the syncer go-routine function which perform the database updates. Internally, it dequeue deferedCommits and +// send the tasks to commitRound for completing the operation. func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { defer close(au.commitSyncerClosed) for { @@ -1237,6 +1250,7 @@ func (au *accountUpdates) commitSyncer(deferedCommits chan deferedCommit) { } } +// commitRound write to the database a "chunk" of rounds, and update the dbRound accordingly. func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookback basics.Round) { defer au.accountsWriting.Done() au.accountsMu.RLock() @@ -1435,10 +1449,12 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb } +// latest returns the latest round func (au *accountUpdates) latest() basics.Round { return au.dbRound + basics.Round(len(au.deltas)) } +// generateCatchpoint generates a single catchpoint file func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label string, committedRoundDigest crypto.Digest, updatingBalancesDuration time.Duration) { beforeGeneratingCatchpointTime := time.Now() catchpointGenerationStats := telemetryspec.CatchpointGenerationEventDetails{ @@ -1532,6 +1548,7 @@ func (au *accountUpdates) generateCatchpoint(committedRound basics.Round, label Infof("Catchpoint file was generated") } +// catchpointRoundToPath calculate the catchpoint file path for a given round func catchpointRoundToPath(rnd basics.Round) string { irnd := int64(rnd) / 256 outStr := "" From 7b4c32181b00a4d7a0205981b80c11e3ef16d2ba Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 25 Jul 2020 20:08:22 -0400 Subject: [PATCH 165/267] Remove redundent ledger.close --- ledger/archival_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index ee1733565e..551b638dd7 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -455,7 +455,6 @@ func TestArchivalCreatables(t *testing.T) { l.Close() l, err = OpenLedger(logging.Base(), dbPrefix, inMem, genesisInitState, cfg) require.NoError(t, err) - defer l.Close() // check that we can fetch creator for all created assets and can't for // deleted assets From 4384a6dd4acd5b8d56a5575b4359b194a9f32159 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Sun, 26 Jul 2020 13:39:50 -0400 Subject: [PATCH 166/267] -- add support for preparing a transaction that is sent from an account that was rekeyed to a msig account. --- cmd/goal/clerk.go | 46 ++++++++++++++++++++++++++++++ cmd/goal/messages.go | 44 ++++++++++++++-------------- daemon/kmd/wallet/driver/sqlite.go | 4 --- 3 files changed, 69 insertions(+), 25 deletions(-) diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 9dd3105e5a..3aabe1c7e4 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -22,6 +22,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "time" @@ -53,6 +54,7 @@ var ( disassemble bool verbose bool progByteFile string + msigParams string logicSigFile string timeStamp int64 protoVersion string @@ -85,6 +87,7 @@ func init() { sendCmd.Flags().StringVarP(&progByteFile, "from-program-bytes", "P", "", "Program binary to use as account logic") sendCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "base64 encoded args to pass to transaction logic") sendCmd.Flags().StringVarP(&logicSigFile, "logic-sig", "L", "", "LogicSig to apply to transaction") + sendCmd.Flags().StringVar(&msigParams, "msig-params", "", "Multisig pre image parameters - [threshold] [Address 1] [Address 2] ...\nUsed to add the necessary fields in case the account was rekeyed to a multisig account") sendCmd.MarkFlagRequired("to") sendCmd.MarkFlagRequired("amount") @@ -277,6 +280,11 @@ var sendCmd = &cobra.Command{ reportErrorln(soFlagError) } + // --msig-params is invalid without -o + if outFilename == "" && msigParams != "" { + reportErrorln(NoOutputFileError) + } + checkTxValidityPeriodCmdFlags(cmd) dataDir := ensureSingleDataDir() @@ -389,6 +397,44 @@ var sendCmd = &cobra.Command{ } } + // Handle the case where the user wants to send to an account that was rekeyed to a multisig account + if msigParams != "" { + // Decode params + params := strings.Split(msigParams, " ") + if len(params) < 3 { + reportErrorf(msigParseError, "Not enough arguments to create the multisig address.\nPlease make sure to specify the threshold and at least 2 addresses\n") + } + + threshold, err := strconv.ParseUint(params[0], 10, 8) + if err != nil { + reportErrorf(msigParseError, "Failed to parse the threshold. Make sure it's a number between 1 and 255") + } + + // Convert the addresses into public keys + pks := make([]crypto.PublicKey, len(params[1:])) + for i, addrStr := range params[1:] { + addr, err := basics.UnmarshalChecksumAddress(addrStr) + if err != nil { + reportErrorf(failDecodeAddressError, err) + } + pks[i] = crypto.PublicKey(addr) + } + + addr, err := crypto.MultisigAddrGen(1, uint8(threshold), pks) + if err != nil { + reportErrorf(msigParseError, err) + } + + // Generate the multisig and assign to the txn + stx.Msig = crypto.MultisigPreimageFromPKs(1, uint8(threshold), pks) + + // Append the signer since it's a rekey txn + if basics.Address(addr) == stx.Txn.Sender { + reportWarnln(rekeySenderTargetSameError) + } + stx.AuthAddr = basics.Address(addr) + } + if outFilename == "" { // Broadcast the tx txid, err := client.BroadcastTransaction(stx) diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index 114b77d649..95d6ef1d20 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -99,27 +99,29 @@ const ( errorClearProgArgsRequired = "Exactly one of --clear-prog or --clear-prog-raw is required" // Clerk - infoTxIssued = "Sent %d MicroAlgos from account %s to address %s, transaction ID: %s. Fee set to %d" - infoTxCommitted = "Transaction %s committed in round %d" - infoTxPending = "Transaction %s still pending as of round %d" - malformedNote = "Cannot base64-decode note %s: %s" - malformedLease = "Cannot base64-decode lease %s: %s" - fileReadError = "Cannot read file %s: %s" - fileWriteError = "Cannot write file %s: %s" - txDecodeError = "Cannot decode transactions from %s: %s" - txDupError = "Duplicate transaction %s in %s" - txLengthError = "Transaction list length mismatch" - txMergeMismatch = "Cannot merge transactions: transaction IDs differ" - txMergeError = "Cannot merge signatures: %v" - txNoFilesError = "No input filenames specified" - soFlagError = "-s is not meaningful without -o" - infoRawTxIssued = "Raw transaction ID %s issued" - txPoolError = "Transaction %s kicked out of local node pool: %s" - addrNoSigError = "Exactly one of --address or --no-sig is required" - msigLookupError = "Could not lookup multisig information: %s" - msigParseError = "Multisig information parsing error: %s" - - infoAutoFeeSet = "Automatically set fee to %d MicroAlgos" + infoTxIssued = "Sent %d MicroAlgos from account %s to address %s, transaction ID: %s. Fee set to %d" + infoTxCommitted = "Transaction %s committed in round %d" + infoTxPending = "Transaction %s still pending as of round %d" + malformedNote = "Cannot base64-decode note %s: %s" + malformedLease = "Cannot base64-decode lease %s: %s" + fileReadError = "Cannot read file %s: %s" + fileWriteError = "Cannot write file %s: %s" + txDecodeError = "Cannot decode transactions from %s: %s" + txDupError = "Duplicate transaction %s in %s" + txLengthError = "Transaction list length mismatch" + txMergeMismatch = "Cannot merge transactions: transaction IDs differ" + txMergeError = "Cannot merge signatures: %v" + txNoFilesError = "No input filenames specified" + soFlagError = "-s is not meaningful without -o" + infoRawTxIssued = "Raw transaction ID %s issued" + txPoolError = "Transaction %s kicked out of local node pool: %s" + addrNoSigError = "Exactly one of --address or --no-sig is required" + msigLookupError = "Could not lookup multisig information: %s" + msigParseError = "Multisig information parsing error: %s" + failDecodeAddressError = "Cannot decode address: %v" + rekeySenderTargetSameError = "The sender and the resulted multisig address are the same" + NoOutputFileError = "--msig-params must be specified with an output file name (-o)" + infoAutoFeeSet = "Automatically set fee to %d MicroAlgos" loggingNotConfigured = "Remote logging is not currently configured and won't be enabled" loggingNotEnabled = "Remote logging is current disabled" diff --git a/daemon/kmd/wallet/driver/sqlite.go b/daemon/kmd/wallet/driver/sqlite.go index 7168fe5dc7..77675d7509 100644 --- a/daemon/kmd/wallet/driver/sqlite.go +++ b/daemon/kmd/wallet/driver/sqlite.go @@ -1198,10 +1198,6 @@ func (sw *SQLiteWallet) MultisigSignTransaction(tx transactions.Transaction, pk if err != nil { return } - if addr != crypto.Digest(tx.Src()) { - err = errMsigWrongAddr - return - } // Check that key is one of the ones in the preimage err = errMsigWrongKey From e1ded04bed5a947b6744ebc8573fe149079d0ca0 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Sun, 26 Jul 2020 13:46:28 -0400 Subject: [PATCH 167/267] -- fix a bug where goal inspect fail to process rekeyed transaction. --- cmd/goal/inspect.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cmd/goal/inspect.go b/cmd/goal/inspect.go index f00b189c22..9b80d5d537 100644 --- a/cmd/goal/inspect.go +++ b/cmd/goal/inspect.go @@ -33,10 +33,11 @@ import ( type inspectSignedTxn struct { _struct struct{} `codec:",omitempty,omitemptyarray"` - Sig crypto.Signature `codec:"sig"` - Msig inspectMultisigSig `codec:"msig"` - Lsig inspectLogicSig `codec:"lsig"` - Txn transactions.Transaction `codec:"txn"` + Sig crypto.Signature `codec:"sig"` + Msig inspectMultisigSig `codec:"msig"` + Lsig inspectLogicSig `codec:"lsig"` + Txn transactions.Transaction `codec:"txn"` + AuthAddr basics.Address `codec:"sgnr"` } // inspectMultisigSig is isomorphic to MultisigSig but uses different @@ -115,19 +116,21 @@ func inspectTxn(stxn transactions.SignedTxn) (sti inspectSignedTxn, err error) { func stxnToInspect(stxn transactions.SignedTxn) inspectSignedTxn { return inspectSignedTxn{ - Txn: stxn.Txn, - Sig: stxn.Sig, - Msig: msigToInspect(stxn.Msig), - Lsig: lsigToInspect(stxn.Lsig), + Txn: stxn.Txn, + Sig: stxn.Sig, + Msig: msigToInspect(stxn.Msig), + Lsig: lsigToInspect(stxn.Lsig), + AuthAddr: stxn.AuthAddr, } } func stxnFromInspect(sti inspectSignedTxn) transactions.SignedTxn { return transactions.SignedTxn{ - Txn: sti.Txn, - Sig: sti.Sig, - Msig: msigFromInspect(sti.Msig), - Lsig: lsigFromInspect(sti.Lsig), + Txn: sti.Txn, + Sig: sti.Sig, + Msig: msigFromInspect(sti.Msig), + Lsig: lsigFromInspect(sti.Lsig), + AuthAddr: sti.AuthAddr, } } From e2124e3c20a3cd9f7bab7facd6430646d5f04569 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 09:54:01 -0400 Subject: [PATCH 168/267] Relax TestLedgerBlockHdrCaching rounds count, as it's not required to test large number of rounds in order to achieve it's goals. --- ledger/ledger_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 01894cc455..3fc7012262 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -876,7 +876,7 @@ func TestLedgerBlockHdrCaching(t *testing.T) { blk := genesisInitState.Block - for i := 0; i < 2000; i++ { + for i := 0; i < 128; i++ { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) err := l.AddBlock(blk, agreement.Certificate{}) From cb3299c494f8f1d2c9fa7166e64594ec1336406e Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Mon, 27 Jul 2020 11:00:55 -0400 Subject: [PATCH 169/267] Fix exit code race in dryrun expect test --- test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index 72f608d793..f477e61659 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -25,11 +25,9 @@ proc TestGoalDryrunExitCode { DRREQ_FILE TEST_PRIMARY_NODE_DIR EXPECTED_STATUS_C spawn goal clerk dryrun-remote -d $TEST_PRIMARY_NODE_DIR -D $DRREQ_FILE -v expect { timeout { ::AlgorandGoal::Abort "goal clerk dryrun-remote timeout" } - $EXPECTED_MESSAGE {puts "message matched"; set MESSAGE_MATCHED 1; close} - eof + $EXPECTED_MESSAGE {puts "message matched"; set MESSAGE_MATCHED 1; exp_continue} + eof { catch wait result; set STATUS_CODE [lindex $result 3]; } } - catch wait result - set STATUS_CODE [lindex $result 3] if { $STATUS_CODE != $EXPECTED_STATUS_CODE } { puts "Exit code: $STATUS_CODE, expected: $EXPECTED_STATUS_CODE" ::AlgorandGoal::Abort "Progran exited with incorrect code" From def4512e0084c37e8311909bbdbc21998327e959 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 13:19:25 -0400 Subject: [PATCH 170/267] staging. --- ledger/accountdb_test.go | 82 +++++++++++++++++++++++++-- ledger/acctupdates_test.go | 12 ++-- ledger/catchpointwriter_test.go | 99 ++++++++++++++++++++++++++++++++- ledger/cow_test.go | 2 +- 4 files changed, 179 insertions(+), 16 deletions(-) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index b331a4db18..533da5eece 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -52,15 +52,85 @@ func randomAccountData(rewardsLevel uint64) basics.AccountData { } data.RewardsBase = rewardsLevel + return data } -func randomAccounts(niter int) map[basics.Address]basics.AccountData { - res := make(map[basics.Address]basics.AccountData) - for i := 0; i < niter; i++ { - res[randomAddress()] = randomAccountData(0) +func randomFullAccountData(rewardsLevel uint64) basics.AccountData { + data := randomAccountData(rewardsLevel) + + crypto.RandBytes(data.VoteID[:]) + crypto.RandBytes(data.SelectionID[:]) + data.VoteFirstValid = basics.Round(crypto.RandUint64()) + data.VoteLastValid = basics.Round(crypto.RandUint64()) + data.VoteKeyDilution = crypto.RandUint64() + if 1 == (crypto.RandUint64() % 2) { + // if account has created assets, have these defined. + data.AssetParams = make(map[basics.AssetIndex]basics.AssetParams) + createdAssetsCount := crypto.RandUint64()%20 + 1 + for i := uint64(0); i < createdAssetsCount; i++ { + ap := basics.AssetParams{ + Total: crypto.RandUint64(), + Decimals: uint32(crypto.RandUint64() % 20), + DefaultFrozen: (crypto.RandUint64()%2 == 0), + UnitName: fmt.Sprintf("un%x", uint32(crypto.RandUint64()%0x7fffffff)), + AssetName: fmt.Sprintf("an%x", uint32(crypto.RandUint64()%0x7fffffff)), + URL: fmt.Sprintf("url%x", uint32(crypto.RandUint64()%0x7fffffff)), + } + crypto.RandBytes(ap.MetadataHash[:]) + crypto.RandBytes(ap.Manager[:]) + crypto.RandBytes(ap.Reserve[:]) + crypto.RandBytes(ap.Freeze[:]) + crypto.RandBytes(ap.Clawback[:]) + data.AssetParams[basics.AssetIndex(crypto.RandUint64()%50000)] = ap + } + } + if 1 == (crypto.RandUint64() % 2) { + // if account owns assets/applications + data.Assets = make(map[basics.AssetIndex]basics.AssetHolding) + ownedAssetsCount := crypto.RandUint64()%20 + 1 + for i := uint64(0); i < ownedAssetsCount; i++ { + ah := basics.AssetHolding{ + Amount: crypto.RandUint64(), + Frozen: (crypto.RandUint64()%2 == 0), + } + data.Assets[basics.AssetIndex(crypto.RandUint64()%50000)] = ah + } } + if 1 == (crypto.RandUint64() % 5) { + crypto.RandBytes(data.AuthAddr[:]) + } + + //fmt.Printf("%v\n", data.SelectionID) + /* + // AppLocalStates stores the local states associated with any applications + // that this account has opted in to. + AppLocalStates map[AppIndex]AppLocalState `codec:"appl,allocbound=encodedMaxAppLocalStates"` + + // AppParams stores the global parameters and state associated with any + // applications that this account has created. + AppParams map[AppIndex]AppParams `codec:"appp,allocbound=encodedMaxAppParams"` + + // TotalAppSchema stores the sum of all of the LocalStateSchemas + // and GlobalStateSchemas in this account (global for applications + // we created local for applications we opted in to), so that we don't + // have to iterate over all of them to compute MinBalance. + TotalAppSchema StateSchema `codec:"tsch"` + */ + return data +} +func randomAccounts(niter int, simpleAccounts bool) map[basics.Address]basics.AccountData { + res := make(map[basics.Address]basics.AccountData) + if simpleAccounts { + for i := 0; i < niter; i++ { + res[randomAddress()] = randomAccountData(0) + } + } else { + for i := 0; i < niter; i++ { + res[randomAddress()] = randomFullAccountData(0) + } + } return res } @@ -177,7 +247,7 @@ func TestAccountDBInit(t *testing.T) { require.NoError(t, err) defer tx.Rollback() - accts := randomAccounts(20) + accts := randomAccounts(20, true) err = accountsInit(tx, accts, proto) require.NoError(t, err) checkAccounts(t, tx, 0, accts) @@ -198,7 +268,7 @@ func TestAccountDBRound(t *testing.T) { require.NoError(t, err) defer tx.Rollback() - accts := randomAccounts(20) + accts := randomAccounts(20, true) err = accountsInit(tx, accts, proto) require.NoError(t, err) checkAccounts(t, tx, 0, accts) diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 6df9ae0e67..4a33ad863a 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -237,7 +237,7 @@ func TestAcctUpdates(t *testing.T) { defer ml.close() ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(20)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(20, true)} rewardsLevels := []uint64{0} pooldata := basics.AccountData{} @@ -318,7 +318,7 @@ func TestAcctUpdatesFastUpdates(t *testing.T) { defer ml.close() ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(20)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(20, true)} rewardsLevels := []uint64{0} pooldata := basics.AccountData{} @@ -411,7 +411,7 @@ func BenchmarkBalancesChanges(b *testing.B) { initialRounds := uint64(1) ml.blocks = randomInitChain(protocolVersion, int(initialRounds)) accountsCount := 5000 - accts := []map[basics.Address]basics.AccountData{randomAccounts(accountsCount)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(accountsCount, true)} rewardsLevels := []uint64{0} pooldata := basics.AccountData{} @@ -542,7 +542,7 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(100000)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(100000, false)} rewardsLevels := []uint64{0} pooldata := basics.AccountData{} @@ -635,7 +635,7 @@ func TestAcctUpdatesUpdatesCorrectness(t *testing.T) { defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(9)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(9, true)} pooldata := basics.AccountData{} pooldata.MicroAlgos.Raw = 1000 * 1000 * 1000 * 1000 @@ -798,7 +798,7 @@ func TestAcctUpdatesDeleteStoredCatchpoints(t *testing.T) { defer ml.close() ml.blocks = randomInitChain(protocol.ConsensusCurrentVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(20)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(20, true)} au := &accountUpdates{} conf := config.GetDefaultLocal() conf.CatchpointInterval = 1 diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index cd1a85732c..466ef3d55c 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -130,15 +130,16 @@ func TestBasicCatchpointWriter(t *testing.T) { protoParams.SeedLookback = 2 protoParams.SeedRefreshInterval = 8 config.Consensus[testProtocolVersion] = protoParams + temporaryDirectroy, _ := ioutil.TempDir(os.TempDir(), "catchpoints") defer func() { delete(config.Consensus, testProtocolVersion) - os.RemoveAll("./catchpoints") + os.RemoveAll(temporaryDirectroy) }() ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(300)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(300, false)} au := &accountUpdates{} conf := config.GetDefaultLocal() @@ -149,7 +150,99 @@ func TestBasicCatchpointWriter(t *testing.T) { err := au.loadFromDisk(ml) require.NoError(t, err) au.close() - fileName := filepath.Join("./catchpoints", "15.catchpoint") + fileName := filepath.Join(temporaryDirectroy, "15.catchpoint") + blocksRound := basics.Round(12345) + blockHeaderDigest := crypto.Hash([]byte{1, 2, 3}) + catchpointLabel := fmt.Sprintf("%d#%v", blocksRound, blockHeaderDigest) // this is not a correct way to create a label, but it's good enough for this unit test + writer := makeCatchpointWriter(fileName, ml.trackerDB().rdb, blocksRound, blockHeaderDigest, catchpointLabel) + for { + more, err := writer.WriteStep(context.Background()) + require.NoError(t, err) + if !more { + break + } + } + + // load the file from disk. + fileContent, err := ioutil.ReadFile(fileName) + require.NoError(t, err) + gzipReader, err := gzip.NewReader(bytes.NewBuffer(fileContent)) + require.NoError(t, err) + tarReader := tar.NewReader(gzipReader) + defer gzipReader.Close() + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + require.NoError(t, err) + break + } + balancesBlockBytes := make([]byte, header.Size) + readComplete := int64(0) + + for readComplete < header.Size { + bytesRead, err := tarReader.Read(balancesBlockBytes[readComplete:]) + readComplete += int64(bytesRead) + if err != nil { + if err == io.EOF { + if readComplete == header.Size { + break + } + require.NoError(t, err) + } + break + } + } + if header.Name == "content.msgpack" { + var fileHeader CatchpointFileHeader + err = protocol.Decode(balancesBlockBytes, &fileHeader) + require.NoError(t, err) + require.Equal(t, catchpointLabel, fileHeader.Catchpoint) + require.Equal(t, blocksRound, fileHeader.BlocksRound) + require.Equal(t, blockHeaderDigest, fileHeader.BlockHeaderDigest) + require.Equal(t, uint64(len(accts[0])), fileHeader.TotalAccounts) + } else if header.Name == "balances.1.1.msgpack" { + var balances catchpointFileBalancesChunk + err = protocol.Decode(balancesBlockBytes, &balances) + require.NoError(t, err) + require.Equal(t, uint64(len(accts[0])), uint64(len(balances.Balances))) + } else { + require.Failf(t, "unexpected tar chunk name %s", header.Name) + } + } +} + +func TestCatchpointWriterWithApplicationsData(t *testing.T) { + // create new protocol version, which has lower back balance. + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestCatchpointWriterWithApplicationsData") + protoParams := config.Consensus[protocol.ConsensusCurrentVersion] + protoParams.MaxBalLookback = 32 + protoParams.SeedLookback = 2 + protoParams.SeedRefreshInterval = 8 + config.Consensus[testProtocolVersion] = protoParams + temporaryDirectroy, _ := ioutil.TempDir(os.TempDir(), "catchpoints") + defer func() { + delete(config.Consensus, testProtocolVersion) + os.RemoveAll(temporaryDirectroy) + }() + + ml := makeMockLedgerForTracker(t, true) + defer ml.close() + ml.blocks = randomInitChain(testProtocolVersion, 10) + accts := []map[basics.Address]basics.AccountData{randomAccounts(300, false)} + + au := &accountUpdates{} + conf := config.GetDefaultLocal() + conf.CatchpointInterval = 1 + conf.Archival = true + au.initialize(conf, ".", protoParams, accts[0]) + defer au.close() + err := au.loadFromDisk(ml) + require.NoError(t, err) + au.close() + fileName := filepath.Join(temporaryDirectroy, "15.catchpoint") blocksRound := basics.Round(12345) blockHeaderDigest := crypto.Hash([]byte{1, 2, 3}) catchpointLabel := fmt.Sprintf("%d#%v", blocksRound, blockHeaderDigest) // this is not a correct way to create a label, but it's good enough for this unit test diff --git a/ledger/cow_test.go b/ledger/cow_test.go index 5438e8e8d0..6b871e45a6 100644 --- a/ledger/cow_test.go +++ b/ledger/cow_test.go @@ -73,7 +73,7 @@ func applyUpdates(cow *roundCowState, updates map[basics.Address]accountDelta) { } func TestCowBalance(t *testing.T) { - accts0 := randomAccounts(20) + accts0 := randomAccounts(20, true) ml := mockLedger{balanceMap: accts0} c0 := makeRoundCowState(&ml, bookkeeping.BlockHeader{}) From fd31e02cb899b879314748f778a22600fad975f0 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 13:26:06 -0400 Subject: [PATCH 171/267] update comment. --- ledger/acctupdates.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 594a8b0259..c56009af2c 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -195,7 +195,7 @@ type accountUpdates struct { // accountsWriting provides syncronization around the background writing of account balances. accountsWriting sync.WaitGroup - // commitSyncerWaitGroup is the blocking channel for syncronizing closing the commitSyncer goroutine. Once it's closed, the + // commitSyncerClosed is the blocking channel for syncronizing closing the commitSyncer goroutine. Once it's closed, the // commitSyncer can be assumed to have aborted. commitSyncerClosed chan struct{} } @@ -222,7 +222,7 @@ func (au *accountUpdates) initialize(cfg config.Local, dbPathPrefix string, gene close(au.commitSyncerClosed) } -// loadFromDisk is the 2nd level initializtion, and is required before the accountUpdates becomes functional +// loadFromDisk is the 2nd level initialization, and is required before the accountUpdates becomes functional // The close function is expected to be call in pair with loadFromDisk func (au *accountUpdates) loadFromDisk(l ledgerForTracker) error { au.accountsMu.Lock() From f59a3695808f6db3e2c46b772a18b01c96b78d21 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 13:38:01 -0400 Subject: [PATCH 172/267] update comment. --- ledger/accountdb.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ledger/accountdb.go b/ledger/accountdb.go index 122ee72d05..cb95516337 100644 --- a/ledger/accountdb.go +++ b/ledger/accountdb.go @@ -618,6 +618,7 @@ func accountsPutTotals(tx *sql.Tx, totals AccountTotals, catchpointStaging bool) return err } +// accountsNewRound updates the accountbase and assetcreators by applying the provided deltas to the accounts / creatables. func accountsNewRound(tx *sql.Tx, updates map[basics.Address]accountDelta, creatables map[basics.CreatableIndex]modifiedCreatable) (err error) { var insertCreatableIdxStmt, deleteCreatableIdxStmt, deleteStmt, replaceStmt *sql.Stmt From 48863c0c2d9bec7d18e909933773f8448f39fe04 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 14:10:21 -0400 Subject: [PATCH 173/267] stage --- ledger/accountdb_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index 533da5eece..895eabd315 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -101,6 +101,37 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { crypto.RandBytes(data.AuthAddr[:]) } + if 1 == (crypto.RandUint64() % 3) { + data.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + appStatesCount := crypto.RandUint64()%20 + 1 + for i := uint64(0); i < appStatesCount; i++ { + ap := basics.AppLocalState{ + Schema: basics.StateSchema{ + NumUint: crypto.RandUint64() % 5, + NumByteSlice: crypto.RandUint64() % 5, + }, + KeyValue: make(map[string]basics.TealValue), + } + appName := fmt.Sprintf("app%x", crypto.RandUint64()) + for i := uint64(0); i < ap.Schema.NumUint; i++ { + ap.KeyValue[appName] = basics.TealValue{ + Type: basics.TealUintType, + Uint: crypto.RandUint64(), + } + } + for i := uint64(0); i < ap.Schema.NumByteSlice; i++ { + tv := basics.TealValue{ + Type: basics.TealBytesType, + } + bytes := make([]byte, crypto.RandUint64()%512) + crypto.RandBytes(bytes[:]) + tv.Bytes = string(bytes) + ap.KeyValue[appName] = tv + } + data.AppLocalStates[basics.AppIndex(crypto.RandUint64()%50000)] = ap + } + } + //fmt.Printf("%v\n", data.SelectionID) /* // AppLocalStates stores the local states associated with any applications From a2099377263512a4b403594b066219efa2ec3060 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 14:27:19 -0400 Subject: [PATCH 174/267] step. --- ledger/accountdb_test.go | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index 895eabd315..c92f738ab8 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -112,7 +112,7 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { }, KeyValue: make(map[string]basics.TealValue), } - appName := fmt.Sprintf("app%x", crypto.RandUint64()) + appName := fmt.Sprintf("lapp%x", crypto.RandUint64()) for i := uint64(0); i < ap.Schema.NumUint; i++ { ap.KeyValue[appName] = basics.TealValue{ Type: basics.TealUintType, @@ -132,21 +132,31 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { } } + if 1 == (crypto.RandUint64() % 3) { + data.TotalAppSchema = basics.StateSchema{ + NumUint: crypto.RandUint64() % 50, + NumByteSlice: crypto.RandUint64() % 50, + } + } + if 1 == (crypto.RandUint64() % 3) { + data.AppParams = make(map[basics.AppIndex]basics.AppParams) + appParamsCount := crypto.RandUint64()%20 + 1 + for i := uint64(0); i < appParamsCount; i++ { + ap := basics.AppParams{ + ApprovalProgram: make([]byte, int(crypto.RandUint64())%config.MaxAppProgramLen), + ClearStateProgram: make([]byte, int(crypto.RandUint64())%config.MaxAppProgramLen), + } + crypto.RandBytes(ap.ApprovalProgram[:]) + crypto.RandBytes(ap.ClearStateProgram[:]) + data.AppParams[basics.AppIndex(crypto.RandUint64()%50000)] = ap + } + + } //fmt.Printf("%v\n", data.SelectionID) /* - // AppLocalStates stores the local states associated with any applications - // that this account has opted in to. - AppLocalStates map[AppIndex]AppLocalState `codec:"appl,allocbound=encodedMaxAppLocalStates"` - // AppParams stores the global parameters and state associated with any // applications that this account has created. AppParams map[AppIndex]AppParams `codec:"appp,allocbound=encodedMaxAppParams"` - - // TotalAppSchema stores the sum of all of the LocalStateSchemas - // and GlobalStateSchemas in this account (global for applications - // we created local for applications we opted in to), so that we don't - // have to iterate over all of them to compute MinBalance. - TotalAppSchema StateSchema `codec:"tsch"` */ return data } From 5715394c2538e792de6b5c6721def164fb670574 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 27 Jul 2020 14:46:44 -0400 Subject: [PATCH 175/267] move transaction apply methods into their own package --- daemon/algod/api/server/v2/dryrun.go | 6 +- data/transactions/application.go | 628 -------- data/transactions/application_test.go | 1295 ---------------- data/transactions/asset.go | 369 ----- data/transactions/keyreg.go | 52 - data/transactions/payment.go | 68 - data/transactions/payment_test.go | 294 ---- data/transactions/transaction.go | 100 -- ledger/applications.go | 10 +- ledger/apply/application.go | 648 ++++++++ ledger/apply/application_test.go | 1378 +++++++++++++++++ ledger/apply/apply.go | 41 + ledger/apply/asset.go | 394 +++++ ledger/apply/keyreg.go | 75 + .../apply}/keyreg_test.go | 21 +- ledger/apply/payment.go | 110 ++ ledger/apply/payment_test.go | 406 +++++ ledger/eval.go | 72 +- 18 files changed, 3140 insertions(+), 2827 deletions(-) create mode 100644 ledger/apply/application.go create mode 100644 ledger/apply/application_test.go create mode 100644 ledger/apply/apply.go create mode 100644 ledger/apply/asset.go create mode 100644 ledger/apply/keyreg.go rename {data/transactions => ledger/apply}/keyreg_test.go (81%) create mode 100644 ledger/apply/payment.go create mode 100644 ledger/apply/payment_test.go diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index eee3d19dab..a2c62a921b 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -254,12 +254,12 @@ func (dl *dryrunLedger) init() error { return nil } -// transactions.Balances interface +// apply.Balances interface func (dl *dryrunLedger) Round() basics.Round { return basics.Round(dl.dr.Round) } -// transactions.Balances interface +// apply.Balances interface func (dl *dryrunLedger) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { // first check accounts from a previous Put() br, ok := dl.accounts[addr] @@ -312,7 +312,7 @@ func (dl *dryrunLedger) Get(addr basics.Address, withPendingRewards bool) (basic return out, nil } -// transactions.Balances interface +// apply.Balances interface func (dl *dryrunLedger) Put(br basics.BalanceRecord) error { if dl.accounts == nil { dl.accounts = make(map[basics.Address]basics.BalanceRecord) diff --git a/data/transactions/application.go b/data/transactions/application.go index 58f35299ed..7753d90098 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -170,286 +170,6 @@ func (ac *ApplicationCallTxnFields) Empty() bool { return true } -// Allocate the map of LocalStates if it is nil, and return a copy. We do *not* -// call clone on each AppLocalState -- callers must do that for any values -// where they intend to modify a contained reference type e.g. KeyValue. -func cloneAppLocalStates(m map[basics.AppIndex]basics.AppLocalState) map[basics.AppIndex]basics.AppLocalState { - res := make(map[basics.AppIndex]basics.AppLocalState, len(m)) - for k, v := range m { - res[k] = v - } - return res -} - -// Allocate the map of AppParams if it is nil, and return a copy. We do *not* -// call clone on each AppParams -- callers must do that for any values where -// they intend to modify a contained reference type e.g. the GlobalState. -func cloneAppParams(m map[basics.AppIndex]basics.AppParams) map[basics.AppIndex]basics.AppParams { - res := make(map[basics.AppIndex]basics.AppParams, len(m)) - for k, v := range m { - res[k] = v - } - return res -} - -// getAppParams fetches the creator address and AppParams for the app index, -// if they exist. It does *not* clone the AppParams, so the returned params -// must not be modified directly. -func getAppParams(balances Balances, aidx basics.AppIndex) (params basics.AppParams, creator basics.Address, exists bool, err error) { - creator, exists, err = balances.GetCreator(basics.CreatableIndex(aidx), basics.AppCreatable) - if err != nil { - return - } - - // App doesn't exist. Not an error, but return straight away - if !exists { - return - } - - record, err := balances.Get(creator, false) - if err != nil { - return - } - - params, ok := record.AppParams[aidx] - if !ok { - // This should never happen. If app exists then we should have - // found the creator successfully. - err = fmt.Errorf("app %d not found in account %s", aidx, creator.String()) - return - } - - return -} - -func applyStateDelta(kv basics.TealKeyValue, stateDelta basics.StateDelta) error { - if kv == nil { - return fmt.Errorf("cannot apply delta to nil TealKeyValue") - } - - // Because the keys of stateDelta each correspond to one existing/new - // key in the key/value store, there can be at most one delta per key. - // Therefore the order that the deltas are applied does not matter. - for key, valueDelta := range stateDelta { - switch valueDelta.Action { - case basics.SetUintAction: - kv[key] = basics.TealValue{ - Type: basics.TealUintType, - Uint: valueDelta.Uint, - } - case basics.SetBytesAction: - kv[key] = basics.TealValue{ - Type: basics.TealBytesType, - Bytes: valueDelta.Bytes, - } - case basics.DeleteAction: - delete(kv, key) - default: - return fmt.Errorf("unknown delta action %d", valueDelta.Action) - } - } - return nil -} - -// applyError is an error type that may be returned by applyEvalDelta in case -// the transaction execution should not fail for a clear state program. This is -// to distinguish failures due to schema violations from failures due to system -// faults (e.g. a failed database read). -type applyError struct { - msg string -} - -func (a *applyError) Error() string { - return a.msg -} - -func isApplyError(err error) bool { - _, ok := err.(*applyError) - return ok -} - -// applyEvalDelta applies a basics.EvalDelta to the app's global key/value -// store as well as a set of local key/value stores. If this function returns -// an error, the transaction must not be committed. -// -// If the delta we are applying was generated by a ClearStateProgram, then a -// failure to apply the delta does not necessarily mean the transaction should -// be rejected. For example, if the delta would exceed a state schema, then -// we don't want to apply the changes, but we also don't want to fail. In these -// situations, we return an applyError. -func (ac *ApplicationCallTxnFields) applyEvalDelta(evalDelta basics.EvalDelta, params basics.AppParams, creator, sender basics.Address, balances Balances, appIdx basics.AppIndex) error { - /* - * 1. Apply GlobalState delta (if any), allocating the key/value store - * if required. - */ - - proto := balances.ConsensusParams() - if len(evalDelta.GlobalDelta) > 0 { - // Clone the parameters so that they are safe to modify - params = params.Clone() - - // Allocate GlobalState if necessary. We need to do this now - // since an empty map will be read as nil from disk - if params.GlobalState == nil { - params.GlobalState = make(basics.TealKeyValue) - } - - // Check that the global state delta isn't breaking any rules regarding - // key/value lengths - err := evalDelta.GlobalDelta.Valid(&proto) - if err != nil { - return &applyError{fmt.Sprintf("cannot apply GlobalState delta: %v", err)} - } - - // Apply the GlobalDelta in place on the cloned copy - err = applyStateDelta(params.GlobalState, evalDelta.GlobalDelta) - if err != nil { - return err - } - - // Make sure we haven't violated the GlobalStateSchema - err = params.GlobalState.SatisfiesSchema(params.GlobalStateSchema) - if err != nil { - return &applyError{fmt.Sprintf("GlobalState for app %d would use too much space: %v", appIdx, err)} - } - } - - /* - * 2. Apply each LocalState delta, fail fast if any affected account - * has not opted in to appIdx or would violate the LocalStateSchema. - * Don't write anything back to the cow yet. - */ - - changes := make(map[basics.Address]basics.AppLocalState, len(evalDelta.LocalDeltas)) - for accountIdx, delta := range evalDelta.LocalDeltas { - // LocalDeltas are keyed by account index [sender, tx.Accounts[0], ...] - addr, err := ac.AddressByIndex(accountIdx, sender) - if err != nil { - return err - } - - // Ensure we did not already receive a non-empty LocalState - // delta for this address, in case the caller passed us an - // invalid EvalDelta - _, ok := changes[addr] - if ok { - return &applyError{fmt.Sprintf("duplicate LocalState delta for %s", addr.String())} - } - - // Zero-length LocalState deltas are not allowed. We should never produce - // them from Eval. - if len(delta) == 0 { - return &applyError{fmt.Sprintf("got zero-length delta for %s, not allowed", addr.String())} - } - - // Check that the local state delta isn't breaking any rules regarding - // key/value lengths - err = delta.Valid(&proto) - if err != nil { - return &applyError{fmt.Sprintf("cannot apply LocalState delta for %s: %v", addr.String(), err)} - } - - record, err := balances.Get(addr, false) - if err != nil { - return err - } - - localState, ok := record.AppLocalStates[appIdx] - if !ok { - return &applyError{fmt.Sprintf("cannot apply LocalState delta to %s: acct has not opted in to app %d", addr.String(), appIdx)} - } - - // Clone LocalState so that we have a copy that is safe to modify - localState = localState.Clone() - - // Allocate localState.KeyValue if necessary. We need to do - // this now since an empty map will be read as nil from disk - if localState.KeyValue == nil { - localState.KeyValue = make(basics.TealKeyValue) - } - - err = applyStateDelta(localState.KeyValue, delta) - if err != nil { - return err - } - - // Make sure we haven't violated the LocalStateSchema - err = localState.KeyValue.SatisfiesSchema(localState.Schema) - if err != nil { - return &applyError{fmt.Sprintf("LocalState for %s for app %d would use too much space: %v", addr.String(), appIdx, err)} - } - - // Stage the change to be committed after all schema checks - changes[addr] = localState - } - - /* - * 3. Write any GlobalState changes back to cow. This should be correct - * even if creator is in the local deltas, because the updated - * fields are different. - */ - - if len(evalDelta.GlobalDelta) > 0 { - record, err := balances.Get(creator, false) - if err != nil { - return err - } - - // Overwrite parameters for this appIdx with our cloned, - // modified params - record.AppParams = cloneAppParams(record.AppParams) - record.AppParams[appIdx] = params - - err = balances.Put(record) - if err != nil { - return err - } - } - - /* - * 4. Write LocalState changes back to cow - */ - - for addr, newLocalState := range changes { - record, err := balances.Get(addr, false) - if err != nil { - return err - } - - record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) - record.AppLocalStates[appIdx] = newLocalState - - err = balances.Put(record) - if err != nil { - return err - } - } - - return nil -} - -func (ac *ApplicationCallTxnFields) checkPrograms(steva StateEvaluator, maxCost int) error { - cost, err := steva.Check(ac.ApprovalProgram) - if err != nil { - return fmt.Errorf("check failed on ApprovalProgram: %v", err) - } - - if cost > maxCost { - return fmt.Errorf("ApprovalProgram too resource intensive. Cost is %d, max %d", cost, maxCost) - } - - cost, err = steva.Check(ac.ClearStateProgram) - if err != nil { - return fmt.Errorf("check failed on ClearStateProgram: %v", err) - } - - if cost > maxCost { - return fmt.Errorf("ClearStateProgram too resource intensive. Cost is %d, max %d", cost, maxCost) - } - - return nil -} - // AddressByIndex converts an integer index into an address associated with the // transaction. Index 0 corresponds to the transaction sender, and an index > 0 // corresponds to an offset into txn.Accounts. Returns an error if the index is @@ -470,351 +190,3 @@ func (ac *ApplicationCallTxnFields) AddressByIndex(accountIdx uint64, sender bas // accountIdx must be in [1, len(ac.Accounts)] return ac.Accounts[accountIdx-1], nil } - -// createApplication writes a new AppParams entry and returns application ID -func (ac *ApplicationCallTxnFields) createApplication( - balances Balances, creator basics.Address, txnCounter uint64, -) (appIdx basics.AppIndex, err error) { - - // Fetch the creator's (sender's) balance record - record, err := balances.Get(creator, false) - if err != nil { - return - } - - // Make sure the creator isn't already at the app creation max - maxAppsCreated := balances.ConsensusParams().MaxAppsCreated - if len(record.AppParams) >= maxAppsCreated { - err = fmt.Errorf("cannot create app for %s: max created apps per acct is %d", creator.String(), maxAppsCreated) - return - } - - // Clone app params, so that we have a copy that is safe to modify - record.AppParams = cloneAppParams(record.AppParams) - - // Allocate the new app params (+ 1 to match Assets Idx namespace) - appIdx = basics.AppIndex(txnCounter + 1) - record.AppParams[appIdx] = basics.AppParams{ - ApprovalProgram: ac.ApprovalProgram, - ClearStateProgram: ac.ClearStateProgram, - StateSchemas: basics.StateSchemas{ - LocalStateSchema: ac.LocalStateSchema, - GlobalStateSchema: ac.GlobalStateSchema, - }, - } - - // Update the cached TotalStateSchema for this account, used - // when computing MinBalance, since the creator has to store - // the global state - totalSchema := record.TotalAppSchema - totalSchema = totalSchema.AddSchema(ac.GlobalStateSchema) - record.TotalAppSchema = totalSchema - - // 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 and continue - err = balances.PutWithCreatable(record, created, nil) - if err != nil { - return 0, err - } - - return -} - -func (ac *ApplicationCallTxnFields) applyClearState( - balances Balances, sender basics.Address, appIdx basics.AppIndex, - ad *ApplyData, steva StateEvaluator, -) error { - // Fetch the application parameters, if they exist - params, creator, exists, err := getAppParams(balances, appIdx) - if err != nil { - return err - } - - record, err := balances.Get(sender, false) - if err != nil { - return err - } - - // Ensure sender actually has LocalState allocated for this app. - // Can't clear out if not currently opted in - _, ok := record.AppLocalStates[appIdx] - if !ok { - return fmt.Errorf("cannot clear state for app %d: account %s is not currently opted in", appIdx, sender.String()) - } - - // If the application still exists... - if exists { - // Execute the ClearStateProgram before we've deleted the LocalState - // for this account. If the ClearStateProgram does not fail, apply any - // state deltas it generated. - pass, evalDelta, err := steva.Eval(params.ClearStateProgram) - if err == nil && pass { - // Program execution may produce some GlobalState and LocalState - // deltas. Apply them, provided they don't exceed the bounds set by - // the GlobalStateSchema and LocalStateSchema. If they do exceed - // those bounds, then don't fail, but also don't apply the changes. - err = ac.applyEvalDelta(evalDelta, params, creator, sender, balances, appIdx) - if err != nil && !isApplyError(err) { - return err - } - - // If we applied the changes, fill in applyData, so that consumers don't - // have to implement a stateful TEAL interpreter to apply state changes - if err == nil { - ad.EvalDelta = evalDelta - } - } else { - // Ignore errors and rejections from the ClearStateProgram - } - - // Fetch the (potentially updated) sender record - record, err = balances.Get(sender, false) - if err != nil { - return err - } - } - - // Update the TotalAppSchema used for MinBalance calculation, - // since the sender no longer has to store LocalState - totalSchema := record.TotalAppSchema - localSchema := record.AppLocalStates[appIdx].Schema - totalSchema = totalSchema.SubSchema(localSchema) - record.TotalAppSchema = totalSchema - - // Deallocate the AppLocalState and finish - record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) - delete(record.AppLocalStates, appIdx) - - return balances.Put(record) -} - -func applyOptIn(balances Balances, sender basics.Address, appIdx basics.AppIndex, params basics.AppParams) error { - record, err := balances.Get(sender, false) - if err != nil { - return err - } - - // If the user has already opted in, fail - _, ok := record.AppLocalStates[appIdx] - if ok { - return fmt.Errorf("account %s has already opted in to app %d", sender.String(), appIdx) - } - - // Make sure the user isn't already at the app opt-in max - maxAppsOptedIn := balances.ConsensusParams().MaxAppsOptedIn - if len(record.AppLocalStates) >= maxAppsOptedIn { - return fmt.Errorf("cannot opt in app %d for %s: max opted-in apps per acct is %d", appIdx, sender.String(), maxAppsOptedIn) - } - - // If the user hasn't opted in yet, allocate LocalState for the app - record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) - record.AppLocalStates[appIdx] = basics.AppLocalState{ - Schema: params.LocalStateSchema, - } - - // Update the TotalAppSchema used for MinBalance calculation, - // since the sender must now store LocalState - totalSchema := record.TotalAppSchema - totalSchema = totalSchema.AddSchema(params.LocalStateSchema) - record.TotalAppSchema = totalSchema - - return balances.Put(record) -} - -func (ac *ApplicationCallTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData, txnCounter uint64, steva StateEvaluator) (err error) { - defer func() { - // If we are returning a non-nil error, then don't return a - // non-empty EvalDelta. Not required for correctness. - if err != nil && ad != nil { - ad.EvalDelta = basics.EvalDelta{} - } - }() - - // Keep track of the application ID we're working on - appIdx := ac.ApplicationID - - // this is not the case in the current code but still probably better to check - if ad == nil { - err = fmt.Errorf("cannot use empty ApplyData") - return - } - - // Specifying an application ID of 0 indicates application creation - if ac.ApplicationID == 0 { - appIdx, err = ac.createApplication(balances, header.Sender, txnCounter) - if err != nil { - return - } - } - - // Fetch the application parameters, if they exist - params, creator, exists, err := getAppParams(balances, appIdx) - if err != nil { - return err - } - - // Ensure that the only operation we can do is ClearState if the application - // does not exist - if !exists && ac.OnCompletion != ClearStateOC { - return fmt.Errorf("only clearing out is supported for applications that do not exist") - } - - // Initialize our TEAL evaluation context. Internally, this manages - // access to balance records for Stateful TEAL programs as a thin - // wrapper around Balances. - // - // Note that at this point in execution, the application might not exist - // (e.g. if it was deleted). In that case, we will pass empty - // params.StateSchemas below. This is OK because if the application is - // deleted, we will never execute its programs. - err = steva.InitLedger(balances, appIdx, params.StateSchemas) - if err != nil { - return err - } - - // If this txn is going to set new programs (either for creation or - // update), check that the programs are valid and not too expensive - if ac.ApplicationID == 0 || ac.OnCompletion == UpdateApplicationOC { - maxCost := balances.ConsensusParams().MaxAppProgramCost - err = ac.checkPrograms(steva, maxCost) - if err != nil { - return err - } - } - - // Clear out our LocalState. In this case, we don't execute the - // ApprovalProgram, since clearing out is always allowed. We only - // execute the ClearStateProgram, whose failures are ignored. - if ac.OnCompletion == ClearStateOC { - return ac.applyClearState(balances, header.Sender, appIdx, ad, steva) - } - - // If this is an OptIn transaction, ensure that the sender has - // LocalState allocated prior to TEAL execution, so that it may be - // initialized in the same transaction. - if ac.OnCompletion == OptInOC { - err = applyOptIn(balances, header.Sender, appIdx, params) - if err != nil { - return err - } - } - - // Execute the Approval program - approved, evalDelta, err := steva.Eval(params.ApprovalProgram) - if err != nil { - return err - } - - if !approved { - return fmt.Errorf("transaction rejected by ApprovalProgram") - } - - // Apply GlobalState and LocalState deltas, provided they don't exceed - // the bounds set by the GlobalStateSchema and LocalStateSchema. - // If they would exceed those bounds, then fail. - err = ac.applyEvalDelta(evalDelta, params, creator, header.Sender, balances, appIdx) - if err != nil { - return err - } - - switch ac.OnCompletion { - case NoOpOC: - // Nothing to do - - case OptInOC: - // Handled above - - case CloseOutOC: - // Closing out of the application. Fetch the sender's balance record - record, err := balances.Get(header.Sender, false) - if err != nil { - return err - } - - // If they haven't opted in, that's an error - localState, ok := record.AppLocalStates[appIdx] - if !ok { - return fmt.Errorf("account %s is not opted in to app %d", header.Sender.String(), appIdx) - } - - // Update the TotalAppSchema used for MinBalance calculation, - // since the sender no longer has to store LocalState - totalSchema := record.TotalAppSchema - totalSchema = totalSchema.SubSchema(localState.Schema) - record.TotalAppSchema = totalSchema - - // Delete the local state - record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) - delete(record.AppLocalStates, appIdx) - - err = balances.Put(record) - if err != nil { - return err - } - - case DeleteApplicationOC: - // Deleting the application. Fetch the creator's balance record - record, err := balances.Get(creator, false) - if err != nil { - return err - } - - // Update the TotalAppSchema used for MinBalance calculation, - // since the creator no longer has to store the GlobalState - totalSchema := record.TotalAppSchema - globalSchema := record.AppParams[appIdx].GlobalStateSchema - totalSchema = totalSchema.SubSchema(globalSchema) - record.TotalAppSchema = totalSchema - - // Delete the AppParams - record.AppParams = cloneAppParams(record.AppParams) - delete(record.AppParams, appIdx) - - // Tell the cow what app we deleted - deleted := &basics.CreatableLocator{ - Creator: creator, - Type: basics.AppCreatable, - Index: basics.CreatableIndex(appIdx), - } - - // Write back to cow - err = balances.PutWithCreatable(record, nil, deleted) - if err != nil { - return err - } - - case UpdateApplicationOC: - // Updating the application. Fetch the creator's balance record - record, err := balances.Get(creator, false) - if err != nil { - return err - } - - // Fill in the new programs - record.AppParams = cloneAppParams(record.AppParams) - params := record.AppParams[appIdx] - params.ApprovalProgram = ac.ApprovalProgram - params.ClearStateProgram = ac.ClearStateProgram - - record.AppParams[appIdx] = params - err = balances.Put(record) - if err != nil { - return err - } - - default: - return fmt.Errorf("invalid application action") - } - - // Fill in applyData, so that consumers don't have to implement a - // stateful TEAL interpreter to apply state changes - ad.EvalDelta = evalDelta - - return nil -} diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 9853897ca0..ab102b665d 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -17,17 +17,13 @@ package transactions import ( - "fmt" - "math/rand" "reflect" "testing" "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/protocol" ) func TestApplicationCallFieldsNotChanged(t *testing.T) { @@ -92,1297 +88,6 @@ func TestApplicationCallFieldsEmpty(t *testing.T) { a.True(ac.Empty()) } -func getRandomAddress(a *require.Assertions) basics.Address { - const rl = 16 - b := make([]byte, rl) - n, err := rand.Read(b) - a.NoError(err) - a.Equal(rl, n) - - address := crypto.Hash(b) - return basics.Address(address) -} - -type testBalances struct { - appCreators map[basics.AppIndex]basics.Address - balances map[basics.Address]basics.AccountData - 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 -} - -type testBalancesPass struct { - testBalances -} - -const appIdxError basics.AppIndex = 0x11223344 -const appIdxOk basics.AppIndex = 1 - -func (b *testBalances) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { - ad, ok := b.balances[addr] - if !ok { - return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") - } - return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil -} - -func (b *testBalances) Put(record basics.BalanceRecord) error { - b.put++ - if b.putBalances == nil { - b.putBalances = make(map[basics.Address]basics.AccountData) - } - b.putBalances[record.Addr] = record.AccountData - return nil -} - -func (b *testBalances) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { - b.putWith++ - if b.putWithBalances == nil { - b.putWithBalances = make(map[basics.Address]basics.AccountData) - } - b.putWithBalances[record.Addr] = record.AccountData - 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) - if aidx == appIdxError { // magic for test - return basics.Address{}, false, fmt.Errorf("mock synthetic error") - } - - creator, ok := b.appCreators[aidx] - return creator, ok, nil - } - return basics.Address{}, false, nil -} - -func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { - return nil -} - -func (b *testBalancesPass) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { - ad, ok := b.balances[addr] - if !ok { - return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") - } - return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil -} - -func (b *testBalancesPass) Put(record basics.BalanceRecord) error { - if b.balances == nil { - b.balances = make(map[basics.Address]basics.AccountData) - } - b.balances[record.Addr] = record.AccountData - return nil -} - -func (b *testBalancesPass) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { - if b.balances == nil { - b.balances = make(map[basics.Address]basics.AccountData) - } - b.balances[record.Addr] = record.AccountData - return nil -} - -func (b *testBalances) ConsensusParams() config.ConsensusParams { - return b.proto -} - -// ResetWrites clears side effects of Put/PutWithCreatable -func (b *testBalances) ResetWrites() { - b.put = 0 - b.putWith = 0 - b.putBalances = nil - b.putWithBalances = nil - b.putWithNew = []basics.CreatableLocator{} - b.putWithDel = []basics.CreatableLocator{} -} - -func (b *testBalances) SetProto(name protocol.ConsensusVersion) { - b.proto = config.Consensus[name] -} - -type testEvaluator struct { - pass bool - delta basics.EvalDelta - appIdx basics.AppIndex -} - -// Eval for tests that fail on program version > 10 and returns pass/delta from its own state rather than running the program -func (e *testEvaluator) Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) { - if len(program) < 1 || program[0] > 10 { - return false, basics.EvalDelta{}, fmt.Errorf("mock eval error") - } - return e.pass, e.delta, nil -} - -// Check for tests that fail on program version > 10 and returns program len as cost -func (e *testEvaluator) Check(program []byte) (cost int, err error) { - if len(program) < 1 || program[0] > 10 { - return 0, fmt.Errorf("mock check error") - } - return len(program), nil -} - -func (e *testEvaluator) InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { - e.appIdx = appIdx - return nil -} - -func TestAppCallApplyDelta(t *testing.T) { - a := require.New(t) - - var tkv basics.TealKeyValue - var sd basics.StateDelta - err := applyStateDelta(tkv, sd) - a.Error(err) - a.Contains(err.Error(), "cannot apply delta to nil TealKeyValue") - - tkv = basics.TealKeyValue{} - err = applyStateDelta(tkv, sd) - a.NoError(err) - a.True(len(tkv) == 0) - - sd = basics.StateDelta{ - "test": basics.ValueDelta{ - Action: basics.DeltaAction(10), - Uint: 0, - }, - } - - err = applyStateDelta(tkv, sd) - a.Error(err) - a.Contains(err.Error(), "unknown delta action") - - sd = basics.StateDelta{ - "test": basics.ValueDelta{ - Action: basics.SetUintAction, - Uint: 1, - }, - } - err = applyStateDelta(tkv, sd) - a.NoError(err) - a.True(len(tkv) == 1) - a.Equal(uint64(1), tkv["test"].Uint) - a.Equal(basics.TealUintType, tkv["test"].Type) - - sd = basics.StateDelta{ - "test": basics.ValueDelta{ - Action: basics.DeleteAction, - }, - } - err = applyStateDelta(tkv, sd) - a.NoError(err) - a.True(len(tkv) == 0) - - // nil bytes - sd = basics.StateDelta{ - "test": basics.ValueDelta{ - Action: basics.SetBytesAction, - }, - } - err = applyStateDelta(tkv, sd) - a.NoError(err) - a.True(len(tkv) == 1) - a.Equal(basics.TealBytesType, tkv["test"].Type) - a.Equal("", tkv["test"].Bytes) - a.Equal(uint64(0), tkv["test"].Uint) - - // check illformed update - sd = basics.StateDelta{ - "test": basics.ValueDelta{ - Action: basics.SetBytesAction, - Uint: 1, - }, - } - err = applyStateDelta(tkv, sd) - a.NoError(err) - a.True(len(tkv) == 1) - a.Equal(basics.TealBytesType, tkv["test"].Type) - a.Equal("", tkv["test"].Bytes) - a.Equal(uint64(0), tkv["test"].Uint) -} - -func TestAppCallCloneEmpty(t *testing.T) { - a := require.New(t) - - var ls map[basics.AppIndex]basics.AppLocalState - cls := cloneAppLocalStates(ls) - a.Equal(0, len(cls)) - - var ap map[basics.AppIndex]basics.AppParams - cap := cloneAppParams(ap) - a.Equal(0, len(cap)) -} - -func TestAppCallGetParam(t *testing.T) { - a := require.New(t) - - var b testBalances - _, _, _, err := getAppParams(&b, appIdxError) - a.Error(err) - - _, _, exist, err := getAppParams(&b, appIdxOk) - a.NoError(err) - a.False(exist) - - creator := getRandomAddress(a) - addr := getRandomAddress(a) - b.appCreators = map[basics.AppIndex]basics.Address{appIdxOk: creator} - b.balances = map[basics.Address]basics.AccountData{addr: {}} - _, _, exist, err = getAppParams(&b, appIdxOk) - a.Error(err) - a.True(exist) - - b.balances[creator] = basics.AccountData{ - AppParams: map[basics.AppIndex]basics.AppParams{}, - } - _, _, exist, err = getAppParams(&b, appIdxOk) - a.Error(err) - a.True(exist) - - b.balances[creator] = basics.AccountData{ - AppParams: map[basics.AppIndex]basics.AppParams{ - appIdxOk: {}, - }, - } - params, cr, exist, err := getAppParams(&b, appIdxOk) - a.NoError(err) - a.True(exist) - a.Equal(creator, cr) - a.Equal(basics.AppParams{}, params) -} - -func TestAppCallAddressByIndex(t *testing.T) { - a := require.New(t) - - sender := getRandomAddress(a) - var ac ApplicationCallTxnFields - addr, err := ac.AddressByIndex(0, sender) - a.NoError(err) - a.Equal(sender, addr) - - addr, err = ac.AddressByIndex(1, sender) - a.Error(err) - a.Contains(err.Error(), "cannot load account[1]") - a.Equal(0, len(ac.Accounts)) - - acc0 := getRandomAddress(a) - ac.Accounts = []basics.Address{acc0} - addr, err = ac.AddressByIndex(1, sender) - a.NoError(err) - a.Equal(acc0, addr) - - addr, err = ac.AddressByIndex(2, sender) - a.Error(err) - a.Contains(err.Error(), "cannot load account[2]") -} - -func TestAppCallCheckPrograms(t *testing.T) { - a := require.New(t) - - var ac ApplicationCallTxnFields - var steva testEvaluator - - err := ac.checkPrograms(&steva, 1) - a.Error(err) - a.Contains(err.Error(), "check failed on ApprovalProgram") - - program := []byte{2, 0x20, 1, 1, 0x22} // version, intcb, int 1 - ac.ApprovalProgram = program - err = ac.checkPrograms(&steva, 1) - a.Error(err) - a.Contains(err.Error(), "ApprovalProgram too resource intensive") - - err = ac.checkPrograms(&steva, 10) - a.Error(err) - a.Contains(err.Error(), "check failed on ClearStateProgram") - - ac.ClearStateProgram = append(ac.ClearStateProgram, program...) - ac.ClearStateProgram = append(ac.ClearStateProgram, program...) - ac.ClearStateProgram = append(ac.ClearStateProgram, program...) - err = ac.checkPrograms(&steva, 10) - a.Error(err) - a.Contains(err.Error(), "ClearStateProgram too resource intensive") - - ac.ClearStateProgram = program - err = ac.checkPrograms(&steva, 10) - a.NoError(err) -} - -func TestAppCallApplyGlobalStateDeltas(t *testing.T) { - a := require.New(t) - - var creator basics.Address - var sender basics.Address - var ac ApplicationCallTxnFields - var ed basics.EvalDelta - var params basics.AppParams - var appIdx basics.AppIndex - var b testBalances - - // check empty input - err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - ed.GlobalDelta = make(basics.StateDelta) - ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} - - // check global on unsupported proto - b.SetProto(protocol.ConsensusV23) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.True(isApplyError(err)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "cannot apply GlobalState delta") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - // check global on supported proto - b.SetProto(protocol.ConsensusFuture) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.True(isApplyError(err)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space", appIdx)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - // check Action=Delete delta on empty params - ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.DeleteAction} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "balance not found") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - // simulate balances.GetCreator and balances.Get get out of sync - // creator received from balances.GetCreator and has app params - // and its balances.Get record is out of sync/not initialized - // ensure even if AppParams were allocated they are empty - creator = getRandomAddress(a) - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(1, b.put) - a.Equal(0, b.putWith) - pad, ok := b.putBalances[creator] - a.True(ok) - // There is a side effect: Action=Delete bypasses all default checks and - // forces AppParams (empty before) to have an entry for appIdx - ap, ok := pad.AppParams[appIdx] - a.True(ok) - a.Equal(0, len(ap.GlobalState)) - // ensure AppParams with pre-allocated fields is stored as empty AppParams{} - enc := protocol.Encode(&ap) - emp := protocol.Encode(&basics.AppParams{}) - a.Equal(len(emp), len(enc)) - // ensure original balance record in the mock was not changed - // this ensure proper cloning and any in-intended in-memory modifications - a.Equal(basics.AccountData{}, b.balances[creator]) - - b.ResetWrites() - - // now check errors with non-default values - b.SetProto(protocol.ConsensusV23) - ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.True(isApplyError(err)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{}, b.balances[creator]) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "cannot apply GlobalState delta") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{}, b.balances[creator]) - - b.SetProto(protocol.ConsensusFuture) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store integer count", appIdx)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{}, b.balances[creator]) - - // try illformed delta - params.GlobalStateSchema = basics.StateSchema{NumUint: 1} - ed.GlobalDelta["bytes"] = basics.ValueDelta{Action: basics.SetBytesAction, Uint: 1} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store bytes count", appIdx)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{}, b.balances[creator]) - - // check a happy case - params.GlobalStateSchema = basics.StateSchema{NumUint: 1, NumByteSlice: 1} - br := 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 - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(1, b.put) - pad, ok = b.putBalances[creator] - a.True(ok) - ap, ok = pad.AppParams[appIdx] - a.True(ok) - a.Equal(2, len(ap.GlobalState)) - a.Equal(basics.TealBytesType, ap.GlobalState["bytes"].Type) - a.Equal(basics.TealUintType, ap.GlobalState["uint"].Type) - a.Equal(uint64(0), ap.GlobalState["bytes"].Uint) - a.Equal(uint64(1), ap.GlobalState["uint"].Uint) - a.Equal("", ap.GlobalState["bytes"].Bytes) - a.Equal("", ap.GlobalState["uint"].Bytes) - a.Equal(br, b.balances[creator]) -} - -func TestAppCallApplyLocalsStateDeltas(t *testing.T) { - a := require.New(t) - - var creator basics.Address = getRandomAddress(a) - var sender basics.Address = getRandomAddress(a) - var ac ApplicationCallTxnFields - var ed basics.EvalDelta - var params basics.AppParams - var appIdx basics.AppIndex - var b testBalances - - b.balances = make(map[basics.Address]basics.AccountData) - ed.LocalDeltas = make(map[uint64]basics.StateDelta) - - err := ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - ed.LocalDeltas[1] = basics.StateDelta{} - - // non-existing account - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - - // empty delta - ac.Accounts = append(ac.Accounts, sender, sender) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - // test duplicates in accounts - b.SetProto(protocol.ConsensusFuture) - ed.LocalDeltas[0] = basics.StateDelta{"uint": basics.ValueDelta{Action: basics.DeleteAction}} - ed.LocalDeltas[1] = basics.StateDelta{"bytes": basics.ValueDelta{Action: basics.DeleteAction}} - b.balances[sender] = basics.AccountData{} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.True(isApplyError(err)) - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{}, b.balances[sender]) - // not opted in - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "acct has not opted in to app") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - // ensure original balance record in the mock was not changed - // this ensure proper cloning and any in-intended in-memory modifications - a.Equal(basics.AccountData{}, b.balances[sender]) - - states := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} - cp := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} - b.balances[sender] = basics.AccountData{ - AppLocalStates: cp, - } - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "duplicate LocalState delta") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - // ensure no changes in original balance record - a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) - - // test valid deltas and accounts - ac.Accounts = nil - states = map[basics.AppIndex]basics.AppLocalState{appIdx: {}} - b.balances[sender] = basics.AccountData{AppLocalStates: states} - ed.LocalDeltas[0] = basics.StateDelta{ - "uint": basics.ValueDelta{Action: basics.SetUintAction, Uint: 1}, - "bytes": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "value"}, - } - delete(ed.LocalDeltas, 1) - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.Error(err) - a.Contains(err.Error(), "would use too much space") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) - - // happy case - states = map[basics.AppIndex]basics.AppLocalState{appIdx: { - Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, - }} - cp = map[basics.AppIndex]basics.AppLocalState{appIdx: { - Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, - }} - b.balances[sender] = basics.AccountData{AppLocalStates: cp} - err = ac.applyEvalDelta(ed, params, creator, sender, &b, appIdx) - a.NoError(err) - a.Equal(1, b.put) - a.Equal(0, b.putWith) - a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) - a.Equal(basics.TealUintType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Type) - a.Equal(basics.TealBytesType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Type) - a.Equal(uint64(1), b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Uint) - a.Equal("value", b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Bytes) -} - -func TestAppCallCreate(t *testing.T) { - a := require.New(t) - - var b testBalances - var txnCounter uint64 = 1 - ac := ApplicationCallTxnFields{} - creator := getRandomAddress(a) - // no balance record - appIdx, err := ac.createApplication(&b, creator, txnCounter) - a.Error(err) - a.Equal(basics.AppIndex(0), appIdx) - - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - appIdx, err = ac.createApplication(&b, creator, txnCounter) - a.Error(err) - a.Contains(err.Error(), "max created apps per acct is") - - b.SetProto(protocol.ConsensusFuture) - ac.ApprovalProgram = []byte{1} - ac.ClearStateProgram = []byte{2} - ac.LocalStateSchema = basics.StateSchema{NumUint: 1} - ac.GlobalStateSchema = basics.StateSchema{NumByteSlice: 1} - appIdx, err = ac.createApplication(&b, creator, txnCounter) - a.NoError(err) - a.Equal(txnCounter+1, uint64(appIdx)) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - nbr, ok := b.putBalances[creator] - a.False(ok) - nbr, ok = b.putWithBalances[creator] - a.True(ok) - params, ok := nbr.AppParams[appIdx] - a.True(ok) - a.Equal(ac.ApprovalProgram, params.ApprovalProgram) - a.Equal(ac.ClearStateProgram, params.ClearStateProgram) - a.Equal(ac.LocalStateSchema, params.LocalStateSchema) - a.Equal(ac.GlobalStateSchema, params.GlobalStateSchema) - a.True(len(b.putWithNew) > 0) -} - -// TestAppCallApplyCreate carefully tracks and validates balance record updates -func TestAppCallApplyCreate(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := creator - ac := ApplicationCallTxnFields{ - ApplicationID: 0, - ApprovalProgram: []byte{1}, - ClearStateProgram: []byte{1}, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var txnCounter uint64 = 1 - var b testBalances - - err := ac.apply(h, &b, spec, nil, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "cannot use empty ApplyData") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - var ad *ApplyData = &ApplyData{} - - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - 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) - - // this test will succeed in creating the app, but then fail - // because the mock balances doesn't update the creators table - // so it will think the app doesn't exist - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "applications that do not exist") - a.Equal(0, b.put) - a.Equal(1, b.putWith) - - createdAppIdx := basics.AppIndex(txnCounter + 1) - b.appCreators = map[basics.AppIndex]basics.Address{createdAppIdx: creator} - - // save the created app info to the side - saved := b.putWithBalances[creator] - - b.ResetWrites() - - // now looking up the creator will succeed, but we reset writes, so - // they won't have the app params - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", createdAppIdx)) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - - b.ResetWrites() - - // now we give the creator the app params again - cp := basics.AccountData{} - cp.AppParams = cloneAppParams(saved.AppParams) - cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) - b.balances[creator] = cp - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "transaction rejected by ApprovalProgram") - a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - // 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] - - b.ResetWrites() - - cp = basics.AccountData{} - cp.AppParams = cloneAppParams(saved.AppParams) - cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) - - steva.pass = true - gd := map[string]basics.ValueDelta{"uint": {Action: basics.DeltaAction(4), Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "cannot apply GlobalState delta") - a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - a.Equal(basics.EvalDelta{}, ad.EvalDelta) - a.Equal(saved, b.balances[creator]) - saved = b.putWithBalances[creator] - - b.ResetWrites() - - cp = basics.AccountData{} - cp.AppParams = cloneAppParams(saved.AppParams) - cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) - - gd = map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - ac.GlobalStateSchema = basics.StateSchema{NumUint: 1} - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "too much space: store integer") - a.Equal(uint64(steva.appIdx), txnCounter+1) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - a.Equal(saved, b.balances[creator]) - saved = b.putWithBalances[creator] - - b.ResetWrites() - - cp = basics.AccountData{} - cp.AppParams = cloneAppParams(saved.AppParams) - cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) - cp.TotalAppSchema = saved.TotalAppSchema - b.balances[creator] = cp - - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.NoError(err) - appIdx := steva.appIdx - a.Equal(uint64(appIdx), txnCounter+1) - a.Equal(1, b.put) - a.Equal(1, b.putWith) - a.Equal(saved, b.balances[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{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) - a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) - a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) - a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) - br = b.putWithBalances[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) - a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) - a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) -} - -// TestAppCallApplyCreateOptIn checks balance record fields without tracking substages -func TestAppCallApplyCreateOptIn(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := creator - ac := ApplicationCallTxnFields{ - ApplicationID: 0, - ApprovalProgram: []byte{1}, - ClearStateProgram: []byte{1}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, - OnCompletion: OptInOC, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - var ad *ApplyData = &ApplyData{} - var b testBalancesPass - - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - b.SetProto(protocol.ConsensusFuture) - b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} - - steva.pass = true - gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.NoError(err) - a.Equal(steva.appIdx, appIdx) - br := b.balances[creator] - a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) - a.Equal([]byte{1}, br.AppParams[appIdx].ClearStateProgram) - a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) - a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) - a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppParams[appIdx].LocalStateSchema) - a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppLocalStates[appIdx].Schema) - a.Equal(basics.StateSchema{NumUint: 1, NumByteSlice: 2}, br.TotalAppSchema) -} - -func TestAppCallOptIn(t *testing.T) { - a := require.New(t) - - sender := getRandomAddress(a) - - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - var b testBalances - ad := basics.AccountData{} - b.balances = map[basics.Address]basics.AccountData{sender: ad} - - var params basics.AppParams - - err := applyOptIn(&b, sender, appIdx, params) - 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 = applyOptIn(&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) - - b.ResetWrites() - - ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) - ad.AppLocalStates[appIdx] = basics.AppLocalState{} - b.balances = map[basics.Address]basics.AccountData{sender: ad} - err = applyOptIn(&b, sender, appIdx, params) - 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() - - delete(ad.AppLocalStates, appIdx) - ad.AppLocalStates[appIdx+1] = basics.AppLocalState{} - b.balances = map[basics.Address]basics.AccountData{sender: ad} - err = applyOptIn(&b, sender, appIdx, params) - a.NoError(err) - a.Equal(1, b.put) - a.Equal(0, b.putWith) - - b.ResetWrites() - - ad.AppLocalStates[appIdx+1] = basics.AppLocalState{ - Schema: basics.StateSchema{NumByteSlice: 1}, - } - ad.TotalAppSchema = basics.StateSchema{NumByteSlice: 1} - params.LocalStateSchema = basics.StateSchema{NumUint: 1} - b.balances = map[basics.Address]basics.AccountData{sender: ad} - err = applyOptIn(&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: {Schema: basics.StateSchema{NumUint: 1}}, - appIdx + 1: {Schema: basics.StateSchema{NumByteSlice: 1}}, - }, - TotalAppSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, - }, - br, - ) -} - -func TestAppCallClearState(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := getRandomAddress(a) - ac := ApplicationCallTxnFields{} - var steva testEvaluator - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - var b testBalances - - ad := &ApplyData{} - b.appCreators = make(map[basics.AppIndex]basics.Address) - b.balances = make(map[basics.Address]basics.AccountData, 2) - b.SetProto(protocol.ConsensusFuture) - - // check app not exist and not opted in - b.balances[sender] = basics.AccountData{} - err := ac.applyClearState(&b, sender, appIdx, ad, &steva) - a.Error(err) - a.Contains(err.Error(), "not currently opted in") - a.Equal(0, b.put) - a.Equal(0, b.putWith) - - // check non-existing app with empty opt-in - b.balances[sender] = basics.AccountData{ - AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, - } - err = ac.applyClearState(&b, sender, appIdx, ad, &steva) - 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) - // check original balance record not changed - br = b.balances[sender] - a.Equal(map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, br.AppLocalStates) - - b.ResetWrites() - - // check non-existing app with non-empty opt-in - b.balances[sender] = basics.AccountData{ - AppLocalStates: map[basics.AppIndex]basics.AppLocalState{ - appIdx: {Schema: basics.StateSchema{NumUint: 10}}, - }, - } - err = ac.applyClearState(&b, sender, appIdx, ad, &steva) - 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) - a.Equal(basics.EvalDelta{}, ad.EvalDelta) - - b.ResetWrites() - - // check existing application with failing ClearStateProgram - b.balances[creator] = basics.AccountData{ - AppParams: map[basics.AppIndex]basics.AppParams{ - appIdx: { - ClearStateProgram: []byte{1}, - StateSchemas: basics.StateSchemas{ - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - }, - }, - }, - } - b.appCreators[appIdx] = creator - - // one put: to opt out - steva.pass = false - gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - err = ac.applyClearState(&b, sender, appIdx, ad, &steva) - 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) - a.Equal(basics.EvalDelta{}, ad.EvalDelta) - - b.ResetWrites() - - // check existing application with successful ClearStateProgram. two - // puts: one to write global state, one to opt out - steva.pass = true - err = ac.applyClearState(&b, sender, appIdx, ad, &steva) - a.NoError(err) - a.Equal(2, b.put) - a.Equal(0, b.putWith) - a.Equal(0, len(br.AppLocalStates)) - a.Equal(basics.StateSchema{}, br.TotalAppSchema) - a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) -} - -func TestAppCallApplyCloseOut(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := getRandomAddress(a) - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - - ac := ApplicationCallTxnFields{ - ApplicationID: appIdx, - OnCompletion: CloseOutOC, - } - params := basics.AppParams{ - ApprovalProgram: []byte{1}, - StateSchemas: basics.StateSchemas{ - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - }, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var ad *ApplyData = &ApplyData{} - var b testBalances - - 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} - - b.SetProto(protocol.ConsensusFuture) - - steva.pass = false - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "transaction rejected by ApprovalProgram") - a.Equal(steva.appIdx, appIdx) - 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 closing on empty sender's balance record - steva.pass = true - b.balances[sender] = basics.AccountData{} - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - 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) - - b.ResetWrites() - - // check a happy case - gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - b.balances[sender] = basics.AccountData{ - AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, - } - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.NoError(err) - a.Equal(2, b.put) - a.Equal(0, b.putWith) - br = b.putBalances[creator] - a.NotEqual(cbr, br) - a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) - br = b.putBalances[sender] - 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) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := getRandomAddress(a) - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - - ac := ApplicationCallTxnFields{ - ApplicationID: appIdx, - OnCompletion: UpdateApplicationOC, - ApprovalProgram: []byte{2}, - ClearStateProgram: []byte{3}, - } - params := basics.AppParams{ - ApprovalProgram: []byte{1}, - StateSchemas: basics.StateSchemas{ - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - }, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var ad *ApplyData = &ApplyData{} - var b testBalances - - 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} - - b.SetProto(protocol.ConsensusFuture) - - steva.pass = false - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "transaction rejected by ApprovalProgram") - a.Equal(steva.appIdx, appIdx) - 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 updating on empty sender's balance record - happy case - steva.pass = true - b.balances[sender] = basics.AccountData{} - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - 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{3}, br.AppParams[appIdx].ClearStateProgram) - a.Equal(basics.EvalDelta{}, ad.EvalDelta) -} - -func TestAppCallApplyDelete(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := getRandomAddress(a) - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - - ac := ApplicationCallTxnFields{ - ApplicationID: appIdx, - OnCompletion: DeleteApplicationOC, - } - params := basics.AppParams{ - ApprovalProgram: []byte{1}, - StateSchemas: basics.StateSchemas{ - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - }, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var ad *ApplyData = &ApplyData{} - var b testBalances - - 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} - - b.SetProto(protocol.ConsensusFuture) - - steva.pass = false - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "transaction rejected by ApprovalProgram") - a.Equal(steva.appIdx, appIdx) - 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 - steva.pass = true - b.balances[sender] = basics.AccountData{} - err = ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.NoError(err) - a.Equal(0, b.put) - a.Equal(1, b.putWith) - 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) -} - -func TestAppCallApplyCreateClearState(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := creator - ac := ApplicationCallTxnFields{ - ApplicationID: 0, - ApprovalProgram: []byte{1}, - ClearStateProgram: []byte{2}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - OnCompletion: ClearStateOC, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - var ad *ApplyData = &ApplyData{} - var b testBalancesPass - - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - b.SetProto(protocol.ConsensusFuture) - b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} - - steva.pass = true - gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - - // check creation on empty balance record - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.Error(err) - a.Contains(err.Error(), "not currently opted in") - a.Equal(steva.appIdx, appIdx) - a.Equal(basics.EvalDelta{}, ad.EvalDelta) - br := b.balances[creator] - a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) - a.Equal([]byte{2}, br.AppParams[appIdx].ClearStateProgram) - a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) - a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) - a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) - a.Equal(basics.TealKeyValue(nil), br.AppParams[appIdx].GlobalState) -} - -func TestAppCallApplyCreateDelete(t *testing.T) { - a := require.New(t) - - creator := getRandomAddress(a) - sender := creator - ac := ApplicationCallTxnFields{ - ApplicationID: 0, - ApprovalProgram: []byte{1}, - ClearStateProgram: []byte{1}, - GlobalStateSchema: basics.StateSchema{NumUint: 1}, - OnCompletion: DeleteApplicationOC, - } - h := Header{ - Sender: sender, - } - var steva testEvaluator - var spec SpecialAddresses - var txnCounter uint64 = 1 - appIdx := basics.AppIndex(txnCounter + 1) - var ad *ApplyData = &ApplyData{} - var b testBalancesPass - - b.balances = make(map[basics.Address]basics.AccountData) - b.balances[creator] = basics.AccountData{} - b.SetProto(protocol.ConsensusFuture) - b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} - - steva.pass = true - gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} - steva.delta = basics.EvalDelta{GlobalDelta: gd} - - // check creation on empty balance record - err := ac.apply(h, &b, spec, ad, txnCounter, &steva) - a.NoError(err) - a.Equal(steva.appIdx, appIdx) - a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) - br := b.balances[creator] - a.Equal(basics.AppParams{}, br.AppParams[appIdx]) -} - func TestEncodedAppTxnAllocationBounds(t *testing.T) { // ensure that all the supported protocols have value limits less or // equal to their corresponding codec allocbounds diff --git a/data/transactions/asset.go b/data/transactions/asset.go index 13f81ef1c9..a7337a7e45 100644 --- a/data/transactions/asset.go +++ b/data/transactions/asset.go @@ -17,8 +17,6 @@ package transactions import ( - "fmt" - "github.com/algorand/go-algorand/data/basics" ) @@ -77,370 +75,3 @@ type AssetFreezeTxnFields struct { // AssetFrozen is the new frozen value. AssetFrozen bool `codec:"afrz"` } - -func cloneAssetHoldings(m map[basics.AssetIndex]basics.AssetHolding) map[basics.AssetIndex]basics.AssetHolding { - res := make(map[basics.AssetIndex]basics.AssetHolding) - for id, val := range m { - res[id] = val - } - return res -} - -func cloneAssetParams(m map[basics.AssetIndex]basics.AssetParams) map[basics.AssetIndex]basics.AssetParams { - res := make(map[basics.AssetIndex]basics.AssetParams) - for id, val := range m { - res[id] = val - } - return res -} - -func getParams(balances Balances, aidx basics.AssetIndex) (params basics.AssetParams, creator basics.Address, err error) { - creator, exists, err := balances.GetCreator(basics.CreatableIndex(aidx), basics.AssetCreatable) - if err != nil { - return - } - - // For assets, anywhere we're attempting to fetch parameters, we are - // assuming that the asset should exist. - if !exists { - err = fmt.Errorf("asset %d does not exist or has been deleted", aidx) - return - } - - creatorRecord, err := balances.Get(creator, false) - if err != nil { - return - } - - params, ok := creatorRecord.AssetParams[aidx] - if !ok { - err = fmt.Errorf("asset index %d not found in account %s", aidx, creator.String()) - return - } - - return -} - -func (cc AssetConfigTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData, txnCounter uint64) error { - if cc.ConfigAsset == 0 { - // Allocating an asset. - record, err := balances.Get(header.Sender, false) - if err != nil { - return err - } - record.Assets = cloneAssetHoldings(record.Assets) - record.AssetParams = cloneAssetParams(record.AssetParams) - - // Ensure index is never zero - newidx := basics.AssetIndex(txnCounter + 1) - - // Sanity check that there isn't an asset with this counter value. - _, present := record.AssetParams[newidx] - if present { - return fmt.Errorf("already found asset with index %d", newidx) - } - - record.AssetParams[newidx] = cc.AssetParams - record.Assets[newidx] = basics.AssetHolding{ - Amount: cc.AssetParams.Total, - } - - if len(record.Assets) > balances.ConsensusParams().MaxAssetsPerAccount { - 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), - } - - return balances.PutWithCreatable(record, created, nil) - } - - // Re-configuration and destroying must be done by the manager key. - params, creator, err := getParams(balances, cc.ConfigAsset) - if err != nil { - return err - } - - if params.Manager.IsZero() || (header.Sender != params.Manager) { - return fmt.Errorf("this transaction should be issued by the manager. It is issued by %v, manager key %v", header.Sender, params.Manager) - } - - record, err := balances.Get(creator, false) - if err != nil { - return err - } - - 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. - if record.Assets[cc.ConfigAsset].Amount != params.Total { - return fmt.Errorf("cannot destroy asset: creator is holding only %d/%d", record.Assets[cc.ConfigAsset].Amount, params.Total) - } - - // Tell the cow what asset we deleted - deleted = &basics.CreatableLocator{ - Creator: creator, - Type: basics.AssetCreatable, - Index: basics.CreatableIndex(cc.ConfigAsset), - } - - delete(record.Assets, cc.ConfigAsset) - delete(record.AssetParams, cc.ConfigAsset) - } else { - // Changing keys in an asset. - if !params.Manager.IsZero() { - params.Manager = cc.AssetParams.Manager - } - if !params.Reserve.IsZero() { - params.Reserve = cc.AssetParams.Reserve - } - if !params.Freeze.IsZero() { - params.Freeze = cc.AssetParams.Freeze - } - if !params.Clawback.IsZero() { - params.Clawback = cc.AssetParams.Clawback - } - - record.AssetParams[cc.ConfigAsset] = params - } - - return balances.PutWithCreatable(record, nil, deleted) -} - -func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { - if amount == 0 { - return nil - } - - snd, err := balances.Get(addr, false) - if err != nil { - return err - } - - snd.Assets = cloneAssetHoldings(snd.Assets) - sndHolding, ok := snd.Assets[asset] - if !ok { - return fmt.Errorf("asset %v missing from %v", asset, addr) - } - - if sndHolding.Frozen && !bypassFreeze { - return fmt.Errorf("asset %v frozen in %v", asset, addr) - } - - newAmount, overflowed := basics.OSub(sndHolding.Amount, amount) - if overflowed { - return fmt.Errorf("underflow on subtracting %d from sender amount %d", amount, sndHolding.Amount) - } - sndHolding.Amount = newAmount - - snd.Assets[asset] = sndHolding - return balances.Put(snd) -} - -func putIn(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { - if amount == 0 { - return nil - } - - rcv, err := balances.Get(addr, false) - if err != nil { - return err - } - - rcv.Assets = cloneAssetHoldings(rcv.Assets) - rcvHolding, ok := rcv.Assets[asset] - if !ok { - return fmt.Errorf("asset %v missing from %v", asset, addr) - } - - if rcvHolding.Frozen && !bypassFreeze { - return fmt.Errorf("asset frozen in recipient") - } - - var overflowed bool - rcvHolding.Amount, overflowed = basics.OAdd(rcvHolding.Amount, amount) - if overflowed { - return fmt.Errorf("overflow on adding %d to receiver amount %d", amount, rcvHolding.Amount) - } - - rcv.Assets[asset] = rcvHolding - return balances.Put(rcv) -} - -func (ct AssetTransferTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData) error { - // Default to sending from the transaction sender's account. - source := header.Sender - clawback := false - - if !ct.AssetSender.IsZero() { - // Clawback transaction. Check that the transaction sender - // is the Clawback address for this asset. - params, _, err := getParams(balances, ct.XferAsset) - if err != nil { - return err - } - - if params.Clawback.IsZero() || (header.Sender != params.Clawback) { - return fmt.Errorf("clawback not allowed: sender %v, clawback %v", header.Sender, params.Clawback) - } - - // Transaction sent from the correct clawback address, - // execute asset transfer from specified source. - source = ct.AssetSender - clawback = true - } - - // Allocate a slot for asset (self-transfer of zero amount). - if ct.AssetAmount == 0 && ct.AssetReceiver == source && !clawback { - snd, err := balances.Get(source, false) - if err != nil { - return err - } - - snd.Assets = cloneAssetHoldings(snd.Assets) - sndHolding, ok := snd.Assets[ct.XferAsset] - if !ok { - // Initialize holding with default Frozen value. - params, _, err := getParams(balances, ct.XferAsset) - if err != nil { - return err - } - - sndHolding.Frozen = params.DefaultFrozen - snd.Assets[ct.XferAsset] = sndHolding - - if len(snd.Assets) > balances.ConsensusParams().MaxAssetsPerAccount { - return fmt.Errorf("too many assets in account: %d > %d", len(snd.Assets), balances.ConsensusParams().MaxAssetsPerAccount) - } - - err = balances.Put(snd) - if err != nil { - return err - } - } - } - - // Actually move the asset. Zero transfers return right away - // without looking up accounts, so it's fine to have a zero transfer - // to an all-zero address (e.g., when the only meaningful part of - // the transaction is the close-to address). Similarly, takeOut and - // putIn will succeed for zero transfers on frozen asset holdings - err := takeOut(balances, source, ct.XferAsset, ct.AssetAmount, clawback) - if err != nil { - return err - } - - err = putIn(balances, ct.AssetReceiver, ct.XferAsset, ct.AssetAmount, clawback) - if err != nil { - return err - } - - if ct.AssetCloseTo != (basics.Address{}) { - // Cannot close by clawback - if clawback { - return fmt.Errorf("cannot close asset by clawback") - } - - // Fetch the sender balance record. We will use this to ensure - // that the sender is not the creator of the asset, and to - // figure out how much of the asset to move. - snd, err := balances.Get(source, false) - if err != nil { - return err - } - - // The creator of the asset cannot close their holding of the - // asset. Check if we are the creator by seeing if there is an - // AssetParams entry for the asset index. - if _, ok := snd.AssetParams[ct.XferAsset]; ok { - return fmt.Errorf("cannot close asset ID in allocating account") - } - - // Fetch our asset holding, which should exist since we're - // closing it out - sndHolding, ok := snd.Assets[ct.XferAsset] - if !ok { - return fmt.Errorf("asset %v not present in account %v", ct.XferAsset, source) - } - - // Fetch the destination balance record to check if we are - // closing out to the creator - dst, err := balances.Get(ct.AssetCloseTo, false) - if err != nil { - return err - } - - // Allow closing out to the asset creator even when frozen. - // If we are closing out 0 units of the asset, then takeOut - // and putIn will short circuit (so bypassFreeze doesn't matter) - _, bypassFreeze := dst.AssetParams[ct.XferAsset] - - // Move the balance out. - err = takeOut(balances, source, ct.XferAsset, sndHolding.Amount, bypassFreeze) - if err != nil { - return err - } - - // Put the balance in. - err = putIn(balances, ct.AssetCloseTo, ct.XferAsset, sndHolding.Amount, bypassFreeze) - if err != nil { - return err - } - - // Delete the slot from the account. - snd, err = balances.Get(source, false) - if err != nil { - return err - } - - snd.Assets = cloneAssetHoldings(snd.Assets) - sndHolding = snd.Assets[ct.XferAsset] - if sndHolding.Amount != 0 { - return fmt.Errorf("asset %v not zero (%d) after closing", ct.XferAsset, sndHolding.Amount) - } - - delete(snd.Assets, ct.XferAsset) - err = balances.Put(snd) - if err != nil { - return err - } - } - - return nil -} - -func (cf AssetFreezeTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData) error { - // Only the Freeze address can change the freeze value. - params, _, err := getParams(balances, cf.FreezeAsset) - if err != nil { - return err - } - - if params.Freeze.IsZero() || (header.Sender != params.Freeze) { - return fmt.Errorf("freeze not allowed: sender %v, freeze %v", header.Sender, params.Freeze) - } - - // Get the account to be frozen/unfrozen. - record, err := balances.Get(cf.FreezeAccount, false) - if err != nil { - return err - } - record.Assets = cloneAssetHoldings(record.Assets) - - holding, ok := record.Assets[cf.FreezeAsset] - if !ok { - return fmt.Errorf("asset not found in account") - } - - holding.Frozen = cf.AssetFrozen - record.Assets[cf.FreezeAsset] = holding - return balances.Put(record) -} diff --git a/data/transactions/keyreg.go b/data/transactions/keyreg.go index be6f531189..b2477dd055 100644 --- a/data/transactions/keyreg.go +++ b/data/transactions/keyreg.go @@ -17,8 +17,6 @@ package transactions import ( - "fmt" - "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" ) @@ -34,53 +32,3 @@ type KeyregTxnFields struct { VoteKeyDilution uint64 `codec:"votekd"` Nonparticipation bool `codec:"nonpart"` } - -// Apply changes the balances according to this transaction. -func (keyreg KeyregTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData) error { - if header.Sender == spec.FeeSink { - return fmt.Errorf("cannot register participation key for fee sink's address %v ", header.Sender) - } - - // Get the user's balance entry - record, err := balances.Get(header.Sender, false) - if err != nil { - return err - } - - // non-participatory accounts cannot be brought online (or offline) - if record.Status == basics.NotParticipating { - return fmt.Errorf("cannot change online/offline status of non-participating account %v", header.Sender) - } - - // Update the registered keys and mark account as online - // (or, if the voting or selection keys are zero, offline/not-participating) - record.VoteID = keyreg.VotePK - record.SelectionID = keyreg.SelectionPK - if (keyreg.VotePK == crypto.OneTimeSignatureVerifier{} || keyreg.SelectionPK == crypto.VRFVerifier{}) { - if keyreg.Nonparticipation { - if balances.ConsensusParams().SupportBecomeNonParticipatingTransactions { - record.Status = basics.NotParticipating - } else { - return fmt.Errorf("transaction tries to mark an account as nonparticipating, but that transaction is not supported") - } - } else { - record.Status = basics.Offline - } - record.VoteFirstValid = 0 - record.VoteLastValid = 0 - record.VoteKeyDilution = 0 - } else { - record.Status = basics.Online - record.VoteFirstValid = keyreg.VoteFirst - record.VoteLastValid = keyreg.VoteLast - record.VoteKeyDilution = keyreg.VoteKeyDilution - } - - // Write the updated entry - err = balances.Put(record) - if err != nil { - return err - } - - return nil -} diff --git a/data/transactions/payment.go b/data/transactions/payment.go index 6cdaa13f4a..984c66a358 100644 --- a/data/transactions/payment.go +++ b/data/transactions/payment.go @@ -53,71 +53,3 @@ func (payment PaymentTxnFields) checkSpender(header Header, spec SpecialAddresse } return nil } - -// Apply changes the balances according to this transaction. -// The ApplyData argument should reflect the changes made by -// apply(). It may already include changes made by the caller -// (i.e., Transaction.Apply), so apply() must update it rather -// than overwriting it. For example, Transaction.Apply() may -// have updated ad.SenderRewards, and this function should only -// add to ad.SenderRewards (if needed), but not overwrite it. -func (payment PaymentTxnFields) apply(header Header, balances Balances, spec SpecialAddresses, ad *ApplyData) error { - // move tx money - if !payment.Amount.IsZero() || payment.Receiver != (basics.Address{}) { - err := balances.Move(header.Sender, payment.Receiver, payment.Amount, &ad.SenderRewards, &ad.ReceiverRewards) - if err != nil { - return err - } - } - - if payment.CloseRemainderTo != (basics.Address{}) { - rec, err := balances.Get(header.Sender, true) - if err != nil { - return err - } - - closeAmount := rec.AccountData.MicroAlgos - ad.ClosingAmount = closeAmount - err = balances.Move(header.Sender, payment.CloseRemainderTo, closeAmount, &ad.SenderRewards, &ad.CloseRewards) - if err != nil { - return err - } - - // Confirm that we have no balance left - rec, err = balances.Get(header.Sender, true) - if !rec.AccountData.MicroAlgos.IsZero() { - return fmt.Errorf("balance %d still not zero after CloseRemainderTo", rec.AccountData.MicroAlgos.Raw) - } - - // Confirm that there is no asset-related state in the account - if len(rec.Assets) > 0 { - return fmt.Errorf("cannot close: %d outstanding assets", len(rec.Assets)) - } - - if len(rec.AssetParams) > 0 { - // This should be impossible because every asset created - // by an account (in AssetParams) must also appear in Assets, - // which we checked above. - return fmt.Errorf("cannot close: %d outstanding created assets", len(rec.AssetParams)) - } - - // Confirm that there is no application-related state remaining - if len(rec.AppLocalStates) > 0 { - return fmt.Errorf("cannot close: %d outstanding applications opted in. Please opt out or clear them", len(rec.AppLocalStates)) - } - - // Can't have created apps remaining either - if len(rec.AppParams) > 0 { - return fmt.Errorf("cannot close: %d outstanding created applications", len(rec.AppParams)) - } - - // Clear out entire account record, to allow the DB to GC it - rec.AccountData = basics.AccountData{} - err = balances.Put(rec) - if err != nil { - return err - } - } - - return nil -} diff --git a/data/transactions/payment_test.go b/data/transactions/payment_test.go index e6e9435267..9a2c5eccea 100644 --- a/data/transactions/payment_test.go +++ b/data/transactions/payment_test.go @@ -21,17 +21,11 @@ import ( "github.com/stretchr/testify/require" - "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/protocol" ) -var spec = SpecialAddresses{ - FeeSink: feeSink, - RewardsPool: poolAddr, -} - func keypair() *crypto.SignatureSecrets { var seed crypto.Seed crypto.RandBytes(seed[:]) @@ -71,291 +65,3 @@ func TestAlgosEncoding(t *testing.T) { panic("decode of bool into MicroAlgos succeeded") } } - -type mockBalances struct { - protocol.ConsensusVersion -} - -func (balances mockBalances) Round() basics.Round { - return basics.Round(8675309) -} - -func (balances mockBalances) PutWithCreatable(basics.BalanceRecord, *basics.CreatableLocator, *basics.CreatableLocator) error { - return nil -} - -func (balances mockBalances) Get(basics.Address, bool) (basics.BalanceRecord, error) { - return basics.BalanceRecord{}, nil -} - -func (balances mockBalances) GetCreator(idx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { - return basics.Address{}, true, nil -} - -func (balances mockBalances) Put(basics.BalanceRecord) error { - return nil -} - -func (balances mockBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards, dstRewards *basics.MicroAlgos) error { - return nil -} - -func (balances mockBalances) ConsensusParams() config.ConsensusParams { - return config.Consensus[balances.ConsensusVersion] -} - -func TestPaymentApply(t *testing.T) { - mockBalV0 := mockBalances{protocol.ConsensusCurrentVersion} - - secretSrc := keypair() - src := basics.Address(secretSrc.SignatureVerifier) - - secretDst := keypair() - dst := basics.Address(secretDst.SignatureVerifier) - - tx := Transaction{ - Type: protocol.PaymentTx, - Header: Header{ - Sender: src, - Fee: basics.MicroAlgos{Raw: 1}, - FirstValid: basics.Round(100), - LastValid: basics.Round(1000), - }, - PaymentTxnFields: PaymentTxnFields{ - Receiver: dst, - Amount: basics.MicroAlgos{Raw: uint64(50)}, - }, - } - _, err := tx.Apply(mockBalV0, nil, SpecialAddresses{FeeSink: feeSink}, 0) - require.NoError(t, err) -} - -func TestCheckSpender(t *testing.T) { - mockBalV0 := mockBalances{protocol.ConsensusCurrentVersion} - mockBalV7 := mockBalances{protocol.ConsensusV7} - - secretSrc := keypair() - src := basics.Address(secretSrc.SignatureVerifier) - - secretDst := keypair() - dst := basics.Address(secretDst.SignatureVerifier) - - tx := Transaction{ - Type: protocol.PaymentTx, - Header: Header{ - Sender: src, - Fee: basics.MicroAlgos{Raw: 1}, - FirstValid: basics.Round(100), - LastValid: basics.Round(1000), - }, - PaymentTxnFields: PaymentTxnFields{ - Receiver: dst, - Amount: basics.MicroAlgos{Raw: uint64(50)}, - }, - } - - tx.Sender = basics.Address(feeSink) - require.Error(t, tx.checkSpender(tx.Header, spec, mockBalV0.ConsensusParams())) - - poolAddr := basics.Address(poolAddr) - tx.Receiver = poolAddr - require.NoError(t, tx.checkSpender(tx.Header, spec, mockBalV0.ConsensusParams())) - - tx.CloseRemainderTo = poolAddr - require.Error(t, tx.checkSpender(tx.Header, spec, mockBalV0.ConsensusParams())) - require.Error(t, tx.checkSpender(tx.Header, spec, mockBalV7.ConsensusParams())) - - tx.Sender = src - require.NoError(t, tx.checkSpender(tx.Header, spec, mockBalV7.ConsensusParams())) -} - -func TestPaymentValidation(t *testing.T) { - payments, _, _, _ := generateTestObjects(100, 50) - genHash := crypto.Digest{0x42} - for i, txn := range payments { - txn.GenesisHash = genHash - payments[i] = txn - } - tc := ExplicitTxnContext{ - Proto: config.Consensus[protocol.ConsensusCurrentVersion], - GenHash: genHash, - } - for _, txn := range payments { - // Lifetime window - tc.ExplicitRound = txn.First() + 1 - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive during lifetime %v", txn) - } - - tc.ExplicitRound = txn.First() - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive at issuance %v", txn) - } - - tc.ExplicitRound = txn.Last() - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive at expiry %v", txn) - } - - tc.ExplicitRound = txn.First() - 1 - if txn.Alive(tc) == nil { - t.Errorf("premature transaction alive %v", txn) - } - - tc.ExplicitRound = txn.Last() + 1 - if txn.Alive(tc) == nil { - t.Errorf("expired transaction alive %v", txn) - } - - // Make a copy of txn, change some fields, be sure the TXID changes. This is not exhaustive. - var txn2 Transaction - txn2 = txn - txn2.Note = []byte{42} - if txn2.ID() == txn.ID() { - t.Errorf("txid does not depend on note") - } - txn2 = txn - txn2.Amount.Raw++ - if txn2.ID() == txn.ID() { - t.Errorf("txid does not depend on amount") - } - txn2 = txn - txn2.Fee.Raw++ - if txn2.ID() == txn.ID() { - t.Errorf("txid does not depend on fee") - } - txn2 = txn - txn2.LastValid++ - if txn2.ID() == txn.ID() { - t.Errorf("txid does not depend on lastvalid") - } - - // Check malformed transactions - largeWindow := txn - largeWindow.LastValid += basics.Round(tc.Proto.MaxTxnLife) - if largeWindow.WellFormed(spec, tc.Proto) == nil { - t.Errorf("transaction with large window %#v verified incorrectly", largeWindow) - } - - badWindow := txn - badWindow.LastValid = badWindow.FirstValid - 1 - if badWindow.WellFormed(spec, tc.Proto) == nil { - t.Errorf("transaction with bad window %#v verified incorrectly", badWindow) - } - - badFee := txn - badFee.Fee = basics.MicroAlgos{} - if badFee.WellFormed(spec, tc.Proto) == nil { - t.Errorf("transaction with no fee %#v verified incorrectly", badFee) - } - } -} - -func TestPaymentSelfClose(t *testing.T) { - secretSrc := keypair() - src := basics.Address(secretSrc.SignatureVerifier) - - secretDst := keypair() - dst := basics.Address(secretDst.SignatureVerifier) - - tx := Transaction{ - Type: protocol.PaymentTx, - Header: Header{ - Sender: src, - Fee: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinTxnFee}, - FirstValid: basics.Round(100), - LastValid: basics.Round(1000), - }, - PaymentTxnFields: PaymentTxnFields{ - Receiver: dst, - Amount: basics.MicroAlgos{Raw: uint64(50)}, - CloseRemainderTo: src, - }, - } - require.Error(t, tx.WellFormed(spec, config.Consensus[protocol.ConsensusCurrentVersion])) -} - -/* -func TestTxnValidation(t *testing.T) { - _, signed, _, _ := generateTestObjects(100, 50) - tc := ExplicitTxnContext{ - Proto: config.Consensus[protocol.ConsensusCurrentVersion], - } - - for i, stxn := range signed { - if stxn.Verify() != nil { - t.Errorf("signed transaction %#v did not verify", stxn) - } - txn := stxn.Transaction.(Payment) - - tc.ExplicitRound = txn.First()+1 - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive during lifetime %v", txn) - } - - tc.ExplicitRound = txn.First() - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive at issuance %v", txn) - } - - tc.ExplicitRound = txn.Last() - if txn.Alive(tc) != nil { - t.Errorf("transaction not alive at expiry %v", txn) - } - - tc.ExplicitRound = txn.First()-1 - if txn.Alive(tc) != nil { - t.Errorf("premature transaction alive %v", txn) - } - - tc.ExplicitRound = txn.Last()+1 - if txn.Alive(tc) != nil { - t.Errorf("expired transaction alive %v", txn) - } - - badSig := stxn - otherTransaction := txn - otherTransaction.Note = []byte{42} - badSig.Transaction = &otherTransaction - badSig.InitCaches() - if badSig.Verify() == nil { - t.Errorf("modified transaction %#v verified incorrectly", badSig) - } - - noSig := stxn - noSig.Sig = crypto.Signature{} - if noSig.Verify() == nil { - t.Errorf("transaction with no signature %#v verified incorrectly", noSig) - } - - largeWindow := stxn - largeWindow.LastValid += basics.Round(config.Protocol.MaxTxnLife) - if largeWindow.Verify() == nil { - t.Errorf("transaction with large window %#v verified incorrectly", largeWindow) - } - - badWindow := txn - badWindow.Payment.LastValid = badWindow.Payment.FirstValid - 1 - if badWindow.Verify() == nil { - t.Errorf("transaction with bad window %#v verified incorrectly", badWindow) - } - - badFee := txn - badFee.Payment.Fee = basics.MicroAlgos{} - if badFee.Verify() == nil { - t.Errorf("transaction with small fee %#v verified incorrectly", badFee) - } - - overflow := txn - overflow.Payment.Amount = basics.MicroAlgos{} - overflow.Payment.Fee = basics.MicroAlgos{Raw: 10} - if overflow.Verify() == nil { - t.Errorf("transaction with overflowing amount %#v verified incorrectly", overflow) - } - - if i > 5 { - break - } - } -} -*/ diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 88539668f6..75f8d3ec42 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -46,41 +46,6 @@ type SpecialAddresses struct { RewardsPool basics.Address } -// Balances allow to move MicroAlgos from one address to another and to update balance records, or to access and modify individual balance records -// After a call to Put (or Move), future calls to Get or Move will reflect the updated balance record(s) -type Balances interface { - // Get looks up the balance record for an address - // If the account is known to be empty, then err should be nil and the returned balance record should have the given address and empty AccountData - // withPendingRewards specifies whether pending rewards should be applied. - // A non-nil error means the lookup is impossible (e.g., if the database doesn't have necessary state anymore) - Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) - - Put(basics.BalanceRecord) error - - // PutWithCreatable is like Put, but should be used when creating or deleting an asset or application. - PutWithCreatable(record basics.BalanceRecord, 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) - - // Move MicroAlgos from one account to another, doing all necessary overflow checking (convenience method) - // TODO: Does this need to be part of the balances interface, or can it just be implemented here as a function that calls Put and Get? - Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error - - // Balances correspond to a Round, which mean that they also correspond - // to a ConsensusParams. This returns those parameters. - ConsensusParams() config.ConsensusParams -} - -// StateEvaluator is an interface that provides some Stateful TEAL -// functionality that may be passed through to Apply from ledger, avoiding a -// circular dependency between the logic and transactions packages -type StateEvaluator interface { - Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) - Check(program []byte) (cost int, err error) - InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error -} - // Header captures the fields common to every transaction type. type Header struct { _struct struct{} `codec:",omitempty,omitemptyarray"` @@ -533,71 +498,6 @@ func (tx Transaction) EstimateEncodedSize() int { return stx.GetEncodedLength() } -// Apply changes the balances according to this transaction. -func (tx Transaction) Apply(balances Balances, steva StateEvaluator, spec SpecialAddresses, ctr uint64) (ad ApplyData, err error) { - params := balances.ConsensusParams() - - // move fee to pool - err = balances.Move(tx.Sender, spec.FeeSink, tx.Fee, &ad.SenderRewards, nil) - if err != nil { - return - } - - // rekeying: update balrecord.AuthAddr to tx.RekeyTo if provided - if (tx.RekeyTo != basics.Address{}) { - var record basics.BalanceRecord - record, err = balances.Get(tx.Sender, false) - if err != nil { - return - } - // Special case: rekeying to the account's actual address just sets balrecord.AuthAddr to 0 - // This saves 32 bytes in your balance record if you want to go back to using your original key - if tx.RekeyTo == tx.Sender { - record.AuthAddr = basics.Address{} - } else { - record.AuthAddr = tx.RekeyTo - } - - err = balances.Put(record) - if err != nil { - return - } - } - - switch tx.Type { - case protocol.PaymentTx: - err = tx.PaymentTxnFields.apply(tx.Header, balances, spec, &ad) - - case protocol.KeyRegistrationTx: - err = tx.KeyregTxnFields.apply(tx.Header, balances, spec, &ad) - - case protocol.AssetConfigTx: - err = tx.AssetConfigTxnFields.apply(tx.Header, balances, spec, &ad, ctr) - - case protocol.AssetTransferTx: - err = tx.AssetTransferTxnFields.apply(tx.Header, balances, spec, &ad) - - case protocol.AssetFreezeTx: - err = tx.AssetFreezeTxnFields.apply(tx.Header, balances, spec, &ad) - - case protocol.ApplicationCallTx: - err = tx.ApplicationCallTxnFields.apply(tx.Header, balances, spec, &ad, ctr, steva) - - default: - err = fmt.Errorf("Unknown transaction type %v", tx.Type) - } - - // If the protocol does not support rewards in ApplyData, - // clear them out. - if !params.RewardsInApplyData { - ad.SenderRewards = basics.MicroAlgos{} - ad.ReceiverRewards = basics.MicroAlgos{} - ad.CloseRewards = basics.MicroAlgos{} - } - - return -} - // TxnContext describes the context in which a transaction can appear // (pretty much, a block, but we don't have the definition of a block // here, since that would be a circular dependency). This is used to diff --git a/ledger/applications.go b/ledger/applications.go index 6ee9e67aa7..8864a20d67 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -20,8 +20,8 @@ import ( "fmt" "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/ledger/apply" ) // AppTealGlobals contains data accessible by the "global" opcode. @@ -41,7 +41,7 @@ type appTealEvaluator struct { // appLedger implements logic.LedgerForLogic type appLedger struct { - balances transactions.Balances + balances apply.Balances appIdx basics.AppIndex schemas basics.StateSchemas AppTealGlobals @@ -69,7 +69,7 @@ func (ae *appTealEvaluator) Check(program []byte) (cost int, err error) { // InitLedger initializes an appLedger, which satisfies the // logic.LedgerForLogic interface. -func (ae *appTealEvaluator) InitLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { +func (ae *appTealEvaluator) InitLedger(balances apply.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { ledger, err := newAppLedger(balances, appIdx, schemas, ae.AppTealGlobals) if err != nil { return err @@ -79,7 +79,7 @@ func (ae *appTealEvaluator) InitLedger(balances transactions.Balances, appIdx ba return nil } -func newAppLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (al *appLedger, err error) { +func newAppLedger(balances apply.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (al *appLedger, err error) { if balances == nil { err = fmt.Errorf("cannot create appLedger with nil balances") return @@ -99,7 +99,7 @@ func newAppLedger(balances transactions.Balances, appIdx basics.AppIndex, schema } // MakeDebugAppLedger returns logic.LedgerForLogic suitable for debug or dryrun -func MakeDebugAppLedger(balances transactions.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (logic.LedgerForLogic, error) { +func MakeDebugAppLedger(balances apply.Balances, appIdx basics.AppIndex, schemas basics.StateSchemas, globals AppTealGlobals) (logic.LedgerForLogic, error) { return newAppLedger(balances, appIdx, schemas, globals) } diff --git a/ledger/apply/application.go b/ledger/apply/application.go new file mode 100644 index 0000000000..874e2ae9e4 --- /dev/null +++ b/ledger/apply/application.go @@ -0,0 +1,648 @@ +// 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 . + +package apply + +import ( + "fmt" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" +) + +// Allocate the map of LocalStates if it is nil, and return a copy. We do *not* +// call clone on each AppLocalState -- callers must do that for any values +// where they intend to modify a contained reference type e.g. KeyValue. +func cloneAppLocalStates(m map[basics.AppIndex]basics.AppLocalState) map[basics.AppIndex]basics.AppLocalState { + res := make(map[basics.AppIndex]basics.AppLocalState, len(m)) + for k, v := range m { + res[k] = v + } + return res +} + +// Allocate the map of AppParams if it is nil, and return a copy. We do *not* +// call clone on each AppParams -- callers must do that for any values where +// they intend to modify a contained reference type e.g. the GlobalState. +func cloneAppParams(m map[basics.AppIndex]basics.AppParams) map[basics.AppIndex]basics.AppParams { + res := make(map[basics.AppIndex]basics.AppParams, len(m)) + for k, v := range m { + res[k] = v + } + return res +} + +// getAppParams fetches the creator address and AppParams for the app index, +// if they exist. It does *not* clone the AppParams, so the returned params +// must not be modified directly. +func getAppParams(balances Balances, aidx basics.AppIndex) (params basics.AppParams, creator basics.Address, exists bool, err error) { + creator, exists, err = balances.GetCreator(basics.CreatableIndex(aidx), basics.AppCreatable) + if err != nil { + return + } + + // App doesn't exist. Not an error, but return straight away + if !exists { + return + } + + record, err := balances.Get(creator, false) + if err != nil { + return + } + + params, ok := record.AppParams[aidx] + if !ok { + // This should never happen. If app exists then we should have + // found the creator successfully. + err = fmt.Errorf("app %d not found in account %s", aidx, creator.String()) + return + } + + return +} + +func applyStateDelta(kv basics.TealKeyValue, stateDelta basics.StateDelta) error { + if kv == nil { + return fmt.Errorf("cannot apply delta to nil TealKeyValue") + } + + // Because the keys of stateDelta each correspond to one existing/new + // key in the key/value store, there can be at most one delta per key. + // Therefore the order that the deltas are applied does not matter. + for key, valueDelta := range stateDelta { + switch valueDelta.Action { + case basics.SetUintAction: + kv[key] = basics.TealValue{ + Type: basics.TealUintType, + Uint: valueDelta.Uint, + } + case basics.SetBytesAction: + kv[key] = basics.TealValue{ + Type: basics.TealBytesType, + Bytes: valueDelta.Bytes, + } + case basics.DeleteAction: + delete(kv, key) + default: + return fmt.Errorf("unknown delta action %d", valueDelta.Action) + } + } + return nil +} + +// applyError is an error type that may be returned by applyEvalDelta in case +// the transaction execution should not fail for a clear state program. This is +// to distinguish failures due to schema violations from failures due to system +// faults (e.g. a failed database read). +type applyError struct { + msg string +} + +func (a *applyError) Error() string { + return a.msg +} + +func isApplyError(err error) bool { + _, ok := err.(*applyError) + return ok +} + +// applyEvalDelta applies a basics.EvalDelta to the app's global key/value +// store as well as a set of local key/value stores. If this function returns +// an error, the transaction must not be committed. +// +// If the delta we are applying was generated by a ClearStateProgram, then a +// failure to apply the delta does not necessarily mean the transaction should +// be rejected. For example, if the delta would exceed a state schema, then +// we don't want to apply the changes, but we also don't want to fail. In these +// situations, we return an applyError. +func applyEvalDelta(ac *transactions.ApplicationCallTxnFields, evalDelta basics.EvalDelta, params basics.AppParams, creator, sender basics.Address, balances Balances, appIdx basics.AppIndex) error { + /* + * 1. Apply GlobalState delta (if any), allocating the key/value store + * if required. + */ + + proto := balances.ConsensusParams() + if len(evalDelta.GlobalDelta) > 0 { + // Clone the parameters so that they are safe to modify + params = params.Clone() + + // Allocate GlobalState if necessary. We need to do this now + // since an empty map will be read as nil from disk + if params.GlobalState == nil { + params.GlobalState = make(basics.TealKeyValue) + } + + // Check that the global state delta isn't breaking any rules regarding + // key/value lengths + err := evalDelta.GlobalDelta.Valid(&proto) + if err != nil { + return &applyError{fmt.Sprintf("cannot apply GlobalState delta: %v", err)} + } + + // Apply the GlobalDelta in place on the cloned copy + err = applyStateDelta(params.GlobalState, evalDelta.GlobalDelta) + if err != nil { + return err + } + + // Make sure we haven't violated the GlobalStateSchema + err = params.GlobalState.SatisfiesSchema(params.GlobalStateSchema) + if err != nil { + return &applyError{fmt.Sprintf("GlobalState for app %d would use too much space: %v", appIdx, err)} + } + } + + /* + * 2. Apply each LocalState delta, fail fast if any affected account + * has not opted in to appIdx or would violate the LocalStateSchema. + * Don't write anything back to the cow yet. + */ + + changes := make(map[basics.Address]basics.AppLocalState, len(evalDelta.LocalDeltas)) + for accountIdx, delta := range evalDelta.LocalDeltas { + // LocalDeltas are keyed by account index [sender, tx.Accounts[0], ...] + addr, err := ac.AddressByIndex(accountIdx, sender) + if err != nil { + return err + } + + // Ensure we did not already receive a non-empty LocalState + // delta for this address, in case the caller passed us an + // invalid EvalDelta + _, ok := changes[addr] + if ok { + return &applyError{fmt.Sprintf("duplicate LocalState delta for %s", addr.String())} + } + + // Zero-length LocalState deltas are not allowed. We should never produce + // them from Eval. + if len(delta) == 0 { + return &applyError{fmt.Sprintf("got zero-length delta for %s, not allowed", addr.String())} + } + + // Check that the local state delta isn't breaking any rules regarding + // key/value lengths + err = delta.Valid(&proto) + if err != nil { + return &applyError{fmt.Sprintf("cannot apply LocalState delta for %s: %v", addr.String(), err)} + } + + record, err := balances.Get(addr, false) + if err != nil { + return err + } + + localState, ok := record.AppLocalStates[appIdx] + if !ok { + return &applyError{fmt.Sprintf("cannot apply LocalState delta to %s: acct has not opted in to app %d", addr.String(), appIdx)} + } + + // Clone LocalState so that we have a copy that is safe to modify + localState = localState.Clone() + + // Allocate localState.KeyValue if necessary. We need to do + // this now since an empty map will be read as nil from disk + if localState.KeyValue == nil { + localState.KeyValue = make(basics.TealKeyValue) + } + + err = applyStateDelta(localState.KeyValue, delta) + if err != nil { + return err + } + + // Make sure we haven't violated the LocalStateSchema + err = localState.KeyValue.SatisfiesSchema(localState.Schema) + if err != nil { + return &applyError{fmt.Sprintf("LocalState for %s for app %d would use too much space: %v", addr.String(), appIdx, err)} + } + + // Stage the change to be committed after all schema checks + changes[addr] = localState + } + + /* + * 3. Write any GlobalState changes back to cow. This should be correct + * even if creator is in the local deltas, because the updated + * fields are different. + */ + + if len(evalDelta.GlobalDelta) > 0 { + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Overwrite parameters for this appIdx with our cloned, + // modified params + record.AppParams = cloneAppParams(record.AppParams) + record.AppParams[appIdx] = params + + err = balances.Put(record) + if err != nil { + return err + } + } + + /* + * 4. Write LocalState changes back to cow + */ + + for addr, newLocalState := range changes { + record, err := balances.Get(addr, false) + if err != nil { + return err + } + + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + record.AppLocalStates[appIdx] = newLocalState + + err = balances.Put(record) + if err != nil { + return err + } + } + + return nil +} + +func checkPrograms(ac *transactions.ApplicationCallTxnFields, steva StateEvaluator, maxCost int) error { + cost, err := steva.Check(ac.ApprovalProgram) + if err != nil { + return fmt.Errorf("check failed on ApprovalProgram: %v", err) + } + + if cost > maxCost { + return fmt.Errorf("ApprovalProgram too resource intensive. Cost is %d, max %d", cost, maxCost) + } + + cost, err = steva.Check(ac.ClearStateProgram) + if err != nil { + return fmt.Errorf("check failed on ClearStateProgram: %v", err) + } + + if cost > maxCost { + return fmt.Errorf("ClearStateProgram too resource intensive. Cost is %d, max %d", cost, maxCost) + } + + return nil +} + +// createApplication writes a new AppParams entry and returns application ID +func createApplication(ac *transactions.ApplicationCallTxnFields, balances Balances, creator basics.Address, txnCounter uint64) (appIdx basics.AppIndex, err error) { + // Fetch the creator's (sender's) balance record + record, err := balances.Get(creator, false) + if err != nil { + return + } + + // Make sure the creator isn't already at the app creation max + maxAppsCreated := balances.ConsensusParams().MaxAppsCreated + if len(record.AppParams) >= maxAppsCreated { + err = fmt.Errorf("cannot create app for %s: max created apps per acct is %d", creator.String(), maxAppsCreated) + return + } + + // Clone app params, so that we have a copy that is safe to modify + record.AppParams = cloneAppParams(record.AppParams) + + // Allocate the new app params (+ 1 to match Assets Idx namespace) + appIdx = basics.AppIndex(txnCounter + 1) + record.AppParams[appIdx] = basics.AppParams{ + ApprovalProgram: ac.ApprovalProgram, + ClearStateProgram: ac.ClearStateProgram, + StateSchemas: basics.StateSchemas{ + LocalStateSchema: ac.LocalStateSchema, + GlobalStateSchema: ac.GlobalStateSchema, + }, + } + + // Update the cached TotalStateSchema for this account, used + // when computing MinBalance, since the creator has to store + // the global state + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.AddSchema(ac.GlobalStateSchema) + record.TotalAppSchema = totalSchema + + // 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 and continue + err = balances.PutWithCreatable(record, created, nil) + if err != nil { + return 0, err + } + + return +} + +func applyClearState(ac *transactions.ApplicationCallTxnFields, balances Balances, sender basics.Address, appIdx basics.AppIndex, ad *transactions.ApplyData, steva StateEvaluator) error { + // Fetch the application parameters, if they exist + params, creator, exists, err := getAppParams(balances, appIdx) + if err != nil { + return err + } + + record, err := balances.Get(sender, false) + if err != nil { + return err + } + + // Ensure sender actually has LocalState allocated for this app. + // Can't clear out if not currently opted in + _, ok := record.AppLocalStates[appIdx] + if !ok { + return fmt.Errorf("cannot clear state for app %d: account %s is not currently opted in", appIdx, sender.String()) + } + + // If the application still exists... + if exists { + // Execute the ClearStateProgram before we've deleted the LocalState + // for this account. If the ClearStateProgram does not fail, apply any + // state deltas it generated. + pass, evalDelta, err := steva.Eval(params.ClearStateProgram) + if err == nil && pass { + // Program execution may produce some GlobalState and LocalState + // deltas. Apply them, provided they don't exceed the bounds set by + // the GlobalStateSchema and LocalStateSchema. If they do exceed + // those bounds, then don't fail, but also don't apply the changes. + err = applyEvalDelta(ac, evalDelta, params, creator, sender, balances, appIdx) + if err != nil && !isApplyError(err) { + return err + } + + // If we applied the changes, fill in applyData, so that consumers don't + // have to implement a stateful TEAL interpreter to apply state changes + if err == nil { + ad.EvalDelta = evalDelta + } + } else { + // Ignore errors and rejections from the ClearStateProgram + } + + // Fetch the (potentially updated) sender record + record, err = balances.Get(sender, false) + if err != nil { + return err + } + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender no longer has to store LocalState + totalSchema := record.TotalAppSchema + localSchema := record.AppLocalStates[appIdx].Schema + totalSchema = totalSchema.SubSchema(localSchema) + record.TotalAppSchema = totalSchema + + // Deallocate the AppLocalState and finish + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + delete(record.AppLocalStates, appIdx) + + return balances.Put(record) +} + +func applyOptIn(balances Balances, sender basics.Address, appIdx basics.AppIndex, params basics.AppParams) error { + record, err := balances.Get(sender, false) + if err != nil { + return err + } + + // If the user has already opted in, fail + _, ok := record.AppLocalStates[appIdx] + if ok { + return fmt.Errorf("account %s has already opted in to app %d", sender.String(), appIdx) + } + + // Make sure the user isn't already at the app opt-in max + maxAppsOptedIn := balances.ConsensusParams().MaxAppsOptedIn + if len(record.AppLocalStates) >= maxAppsOptedIn { + return fmt.Errorf("cannot opt in app %d for %s: max opted-in apps per acct is %d", appIdx, sender.String(), maxAppsOptedIn) + } + + // If the user hasn't opted in yet, allocate LocalState for the app + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + record.AppLocalStates[appIdx] = basics.AppLocalState{ + Schema: params.LocalStateSchema, + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender must now store LocalState + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.AddSchema(params.LocalStateSchema) + record.TotalAppSchema = totalSchema + + return balances.Put(record) +} + +// ApplicationCall applies an ApplicationCall transaction using the Balances +// interface, recording key/value side effects inside of ApplyData. +func ApplicationCall(ac transactions.ApplicationCallTxnFields, header transactions.Header, balances Balances, ad *transactions.ApplyData, txnCounter uint64, steva StateEvaluator) (err error) { + defer func() { + // If we are returning a non-nil error, then don't return a + // non-empty EvalDelta. Not required for correctness. + if err != nil && ad != nil { + ad.EvalDelta = basics.EvalDelta{} + } + }() + + // Keep track of the application ID we're working on + appIdx := ac.ApplicationID + + // this is not the case in the current code but still probably better to check + if ad == nil { + err = fmt.Errorf("cannot use empty ApplyData") + return + } + + // Specifying an application ID of 0 indicates application creation + if ac.ApplicationID == 0 { + appIdx, err = createApplication(&ac, balances, header.Sender, txnCounter) + if err != nil { + return + } + } + + // Fetch the application parameters, if they exist + params, creator, exists, err := getAppParams(balances, appIdx) + if err != nil { + return err + } + + // Ensure that the only operation we can do is ClearState if the application + // does not exist + if !exists && ac.OnCompletion != transactions.ClearStateOC { + return fmt.Errorf("only clearing out is supported for applications that do not exist") + } + + // Initialize our TEAL evaluation context. Internally, this manages + // access to balance records for Stateful TEAL programs as a thin + // wrapper around Balances. + // + // Note that at this point in execution, the application might not exist + // (e.g. if it was deleted). In that case, we will pass empty + // params.StateSchemas below. This is OK because if the application is + // deleted, we will never execute its programs. + err = steva.InitLedger(balances, appIdx, params.StateSchemas) + if err != nil { + return err + } + + // If this txn is going to set new programs (either for creation or + // update), check that the programs are valid and not too expensive + if ac.ApplicationID == 0 || ac.OnCompletion == transactions.UpdateApplicationOC { + maxCost := balances.ConsensusParams().MaxAppProgramCost + err = checkPrograms(&ac, steva, maxCost) + if err != nil { + return err + } + } + + // Clear out our LocalState. In this case, we don't execute the + // ApprovalProgram, since clearing out is always allowed. We only + // execute the ClearStateProgram, whose failures are ignored. + if ac.OnCompletion == transactions.ClearStateOC { + return applyClearState(&ac, balances, header.Sender, appIdx, ad, steva) + } + + // If this is an OptIn transaction, ensure that the sender has + // LocalState allocated prior to TEAL execution, so that it may be + // initialized in the same transaction. + if ac.OnCompletion == transactions.OptInOC { + err = applyOptIn(balances, header.Sender, appIdx, params) + if err != nil { + return err + } + } + + // Execute the Approval program + approved, evalDelta, err := steva.Eval(params.ApprovalProgram) + if err != nil { + return err + } + + if !approved { + return fmt.Errorf("transaction rejected by ApprovalProgram") + } + + // Apply GlobalState and LocalState deltas, provided they don't exceed + // the bounds set by the GlobalStateSchema and LocalStateSchema. + // If they would exceed those bounds, then fail. + err = applyEvalDelta(&ac, evalDelta, params, creator, header.Sender, balances, appIdx) + if err != nil { + return err + } + + switch ac.OnCompletion { + case transactions.NoOpOC: + // Nothing to do + + case transactions.OptInOC: + // Handled above + + case transactions.CloseOutOC: + // Closing out of the application. Fetch the sender's balance record + record, err := balances.Get(header.Sender, false) + if err != nil { + return err + } + + // If they haven't opted in, that's an error + localState, ok := record.AppLocalStates[appIdx] + if !ok { + return fmt.Errorf("account %s is not opted in to app %d", header.Sender.String(), appIdx) + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the sender no longer has to store LocalState + totalSchema := record.TotalAppSchema + totalSchema = totalSchema.SubSchema(localState.Schema) + record.TotalAppSchema = totalSchema + + // Delete the local state + record.AppLocalStates = cloneAppLocalStates(record.AppLocalStates) + delete(record.AppLocalStates, appIdx) + + err = balances.Put(record) + if err != nil { + return err + } + + case transactions.DeleteApplicationOC: + // Deleting the application. Fetch the creator's balance record + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Update the TotalAppSchema used for MinBalance calculation, + // since the creator no longer has to store the GlobalState + totalSchema := record.TotalAppSchema + globalSchema := record.AppParams[appIdx].GlobalStateSchema + totalSchema = totalSchema.SubSchema(globalSchema) + record.TotalAppSchema = totalSchema + + // Delete the AppParams + record.AppParams = cloneAppParams(record.AppParams) + delete(record.AppParams, appIdx) + + // Tell the cow what app we deleted + deleted := &basics.CreatableLocator{ + Creator: creator, + Type: basics.AppCreatable, + Index: basics.CreatableIndex(appIdx), + } + + // Write back to cow + err = balances.PutWithCreatable(record, nil, deleted) + if err != nil { + return err + } + + case transactions.UpdateApplicationOC: + // Updating the application. Fetch the creator's balance record + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + // Fill in the new programs + record.AppParams = cloneAppParams(record.AppParams) + params := record.AppParams[appIdx] + params.ApprovalProgram = ac.ApprovalProgram + params.ClearStateProgram = ac.ClearStateProgram + + record.AppParams[appIdx] = params + err = balances.Put(record) + if err != nil { + return err + } + + default: + return fmt.Errorf("invalid application action") + } + + // Fill in applyData, so that consumers don't have to implement a + // stateful TEAL interpreter to apply state changes + ad.EvalDelta = evalDelta + + return nil +} diff --git a/ledger/apply/application_test.go b/ledger/apply/application_test.go new file mode 100644 index 0000000000..21aedcc6c8 --- /dev/null +++ b/ledger/apply/application_test.go @@ -0,0 +1,1378 @@ +// 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 . + +package apply + +import ( + "fmt" + "math/rand" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/protocol" +) + +func TestApplicationCallFieldsNotChanged(t *testing.T) { + af := transactions.ApplicationCallTxnFields{} + s := reflect.ValueOf(&af).Elem() + + if s.NumField() != 10 { + t.Errorf("You added or removed a field from transactions.ApplicationCallTxnFields. " + + "Please ensure you have updated the Empty() method and then " + + "fix this test") + } +} + +func TestApplicationCallFieldsEmpty(t *testing.T) { + a := require.New(t) + + ac := transactions.ApplicationCallTxnFields{} + a.True(ac.Empty()) + + ac.ApplicationID = 1 + a.False(ac.Empty()) + + ac.ApplicationID = 0 + ac.OnCompletion = 1 + a.False(ac.Empty()) + + ac.OnCompletion = 0 + ac.ApplicationArgs = make([][]byte, 1) + a.False(ac.Empty()) + + ac.ApplicationArgs = nil + ac.Accounts = make([]basics.Address, 1) + a.False(ac.Empty()) + + ac.Accounts = nil + ac.ForeignApps = make([]basics.AppIndex, 1) + a.False(ac.Empty()) + + ac.ForeignApps = nil + ac.LocalStateSchema = basics.StateSchema{NumUint: 1} + a.False(ac.Empty()) + + ac.LocalStateSchema = basics.StateSchema{} + ac.GlobalStateSchema = basics.StateSchema{NumUint: 1} + a.False(ac.Empty()) + + ac.GlobalStateSchema = basics.StateSchema{} + ac.ApprovalProgram = []byte{1} + a.False(ac.Empty()) + + ac.ApprovalProgram = []byte{} + a.False(ac.Empty()) + + ac.ApprovalProgram = nil + ac.ClearStateProgram = []byte{1} + a.False(ac.Empty()) + + ac.ClearStateProgram = []byte{} + a.False(ac.Empty()) + + ac.ClearStateProgram = nil + a.True(ac.Empty()) +} + +func getRandomAddress(a *require.Assertions) basics.Address { + const rl = 16 + b := make([]byte, rl) + n, err := rand.Read(b) + a.NoError(err) + a.Equal(rl, n) + + address := crypto.Hash(b) + return basics.Address(address) +} + +type testBalances struct { + appCreators map[basics.AppIndex]basics.Address + balances map[basics.Address]basics.AccountData + 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 +} + +type testBalancesPass struct { + testBalances +} + +const appIdxError basics.AppIndex = 0x11223344 +const appIdxOk basics.AppIndex = 1 + +func (b *testBalances) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalances) Put(record basics.BalanceRecord) error { + b.put++ + if b.putBalances == nil { + b.putBalances = make(map[basics.Address]basics.AccountData) + } + b.putBalances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalances) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { + b.putWith++ + if b.putWithBalances == nil { + b.putWithBalances = make(map[basics.Address]basics.AccountData) + } + b.putWithBalances[record.Addr] = record.AccountData + 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) + if aidx == appIdxError { // magic for test + return basics.Address{}, false, fmt.Errorf("mock synthetic error") + } + + creator, ok := b.appCreators[aidx] + return creator, ok, nil + } + return basics.Address{}, false, nil +} + +func (b *testBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error { + return nil +} + +func (b *testBalancesPass) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { + ad, ok := b.balances[addr] + if !ok { + return basics.BalanceRecord{}, fmt.Errorf("mock balance not found") + } + return basics.BalanceRecord{Addr: addr, AccountData: ad}, nil +} + +func (b *testBalancesPass) Put(record basics.BalanceRecord) error { + if b.balances == nil { + b.balances = make(map[basics.Address]basics.AccountData) + } + b.balances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalancesPass) PutWithCreatable(record basics.BalanceRecord, newCreatable *basics.CreatableLocator, deletedCreatable *basics.CreatableLocator) error { + if b.balances == nil { + b.balances = make(map[basics.Address]basics.AccountData) + } + b.balances[record.Addr] = record.AccountData + return nil +} + +func (b *testBalances) ConsensusParams() config.ConsensusParams { + return b.proto +} + +// ResetWrites clears side effects of Put/PutWithCreatable +func (b *testBalances) ResetWrites() { + b.put = 0 + b.putWith = 0 + b.putBalances = nil + b.putWithBalances = nil + b.putWithNew = []basics.CreatableLocator{} + b.putWithDel = []basics.CreatableLocator{} +} + +func (b *testBalances) SetProto(name protocol.ConsensusVersion) { + b.proto = config.Consensus[name] +} + +type testEvaluator struct { + pass bool + delta basics.EvalDelta + appIdx basics.AppIndex +} + +// Eval for tests that fail on program version > 10 and returns pass/delta from its own state rather than running the program +func (e *testEvaluator) Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) { + if len(program) < 1 || program[0] > 10 { + return false, basics.EvalDelta{}, fmt.Errorf("mock eval error") + } + return e.pass, e.delta, nil +} + +// Check for tests that fail on program version > 10 and returns program len as cost +func (e *testEvaluator) Check(program []byte) (cost int, err error) { + if len(program) < 1 || program[0] > 10 { + return 0, fmt.Errorf("mock check error") + } + return len(program), nil +} + +func (e *testEvaluator) InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error { + e.appIdx = appIdx + return nil +} + +func TestAppCallApplyDelta(t *testing.T) { + a := require.New(t) + + var tkv basics.TealKeyValue + var sd basics.StateDelta + err := applyStateDelta(tkv, sd) + a.Error(err) + a.Contains(err.Error(), "cannot apply delta to nil TealKeyValue") + + tkv = basics.TealKeyValue{} + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 0) + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.DeltaAction(10), + Uint: 0, + }, + } + + err = applyStateDelta(tkv, sd) + a.Error(err) + a.Contains(err.Error(), "unknown delta action") + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetUintAction, + Uint: 1, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(uint64(1), tkv["test"].Uint) + a.Equal(basics.TealUintType, tkv["test"].Type) + + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.DeleteAction, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 0) + + // nil bytes + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetBytesAction, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(basics.TealBytesType, tkv["test"].Type) + a.Equal("", tkv["test"].Bytes) + a.Equal(uint64(0), tkv["test"].Uint) + + // check illformed update + sd = basics.StateDelta{ + "test": basics.ValueDelta{ + Action: basics.SetBytesAction, + Uint: 1, + }, + } + err = applyStateDelta(tkv, sd) + a.NoError(err) + a.True(len(tkv) == 1) + a.Equal(basics.TealBytesType, tkv["test"].Type) + a.Equal("", tkv["test"].Bytes) + a.Equal(uint64(0), tkv["test"].Uint) +} + +func TestAppCallCloneEmpty(t *testing.T) { + a := require.New(t) + + var ls map[basics.AppIndex]basics.AppLocalState + cls := cloneAppLocalStates(ls) + a.Equal(0, len(cls)) + + var ap map[basics.AppIndex]basics.AppParams + cap := cloneAppParams(ap) + a.Equal(0, len(cap)) +} + +func TestAppCallGetParam(t *testing.T) { + a := require.New(t) + + var b testBalances + _, _, _, err := getAppParams(&b, appIdxError) + a.Error(err) + + _, _, exist, err := getAppParams(&b, appIdxOk) + a.NoError(err) + a.False(exist) + + creator := getRandomAddress(a) + addr := getRandomAddress(a) + b.appCreators = map[basics.AppIndex]basics.Address{appIdxOk: creator} + b.balances = map[basics.Address]basics.AccountData{addr: {}} + _, _, exist, err = getAppParams(&b, appIdxOk) + a.Error(err) + a.True(exist) + + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{}, + } + _, _, exist, err = getAppParams(&b, appIdxOk) + a.Error(err) + a.True(exist) + + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdxOk: {}, + }, + } + params, cr, exist, err := getAppParams(&b, appIdxOk) + a.NoError(err) + a.True(exist) + a.Equal(creator, cr) + a.Equal(basics.AppParams{}, params) +} + +func TestAppCallAddressByIndex(t *testing.T) { + a := require.New(t) + + sender := getRandomAddress(a) + var ac transactions.ApplicationCallTxnFields + addr, err := ac.AddressByIndex(0, sender) + a.NoError(err) + a.Equal(sender, addr) + + addr, err = ac.AddressByIndex(1, sender) + a.Error(err) + a.Contains(err.Error(), "cannot load account[1]") + a.Equal(0, len(ac.Accounts)) + + acc0 := getRandomAddress(a) + ac.Accounts = []basics.Address{acc0} + addr, err = ac.AddressByIndex(1, sender) + a.NoError(err) + a.Equal(acc0, addr) + + addr, err = ac.AddressByIndex(2, sender) + a.Error(err) + a.Contains(err.Error(), "cannot load account[2]") +} + +func TestAppCallCheckPrograms(t *testing.T) { + a := require.New(t) + + var ac transactions.ApplicationCallTxnFields + var steva testEvaluator + + err := checkPrograms(&ac, &steva, 1) + a.Error(err) + a.Contains(err.Error(), "check failed on ApprovalProgram") + + program := []byte{2, 0x20, 1, 1, 0x22} // version, intcb, int 1 + ac.ApprovalProgram = program + err = checkPrograms(&ac, &steva, 1) + a.Error(err) + a.Contains(err.Error(), "ApprovalProgram too resource intensive") + + err = checkPrograms(&ac, &steva, 10) + a.Error(err) + a.Contains(err.Error(), "check failed on ClearStateProgram") + + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + ac.ClearStateProgram = append(ac.ClearStateProgram, program...) + err = checkPrograms(&ac, &steva, 10) + a.Error(err) + a.Contains(err.Error(), "ClearStateProgram too resource intensive") + + ac.ClearStateProgram = program + err = checkPrograms(&ac, &steva, 10) + a.NoError(err) +} + +func TestAppCallApplyGlobalStateDeltas(t *testing.T) { + a := require.New(t) + + var creator basics.Address + var sender basics.Address + var ac transactions.ApplicationCallTxnFields + var ed basics.EvalDelta + var params basics.AppParams + var appIdx basics.AppIndex + var b testBalances + + // check empty input + err := applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + ed.GlobalDelta = make(basics.StateDelta) + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} + + // check global on unsupported proto + b.SetProto(protocol.ConsensusV23) + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check global on supported proto + b.SetProto(protocol.ConsensusFuture) + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check Action=Delete delta on empty params + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.DeleteAction} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "balance not found") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // simulate balances.GetCreator and balances.Get get out of sync + // creator received from balances.GetCreator and has app params + // and its balances.Get record is out of sync/not initialized + // ensure even if AppParams were allocated they are empty + creator = getRandomAddress(a) + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + pad, ok := b.putBalances[creator] + a.True(ok) + // There is a side effect: Action=Delete bypasses all default checks and + // forces AppParams (empty before) to have an entry for appIdx + ap, ok := pad.AppParams[appIdx] + a.True(ok) + a.Equal(0, len(ap.GlobalState)) + // ensure AppParams with pre-allocated fields is stored as empty AppParams{} + enc := protocol.Encode(&ap) + emp := protocol.Encode(&basics.AppParams{}) + a.Equal(len(emp), len(enc)) + // ensure original balance record in the mock was not changed + // this ensure proper cloning and any in-intended in-memory modifications + a.Equal(basics.AccountData{}, b.balances[creator]) + + b.ResetWrites() + + // now check errors with non-default values + b.SetProto(protocol.ConsensusV23) + ed.GlobalDelta["uint"] = basics.ValueDelta{Action: basics.SetUintAction, Uint: 1} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + b.SetProto(protocol.ConsensusFuture) + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store integer count", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + // try illformed delta + params.GlobalStateSchema = basics.StateSchema{NumUint: 1} + ed.GlobalDelta["bytes"] = basics.ValueDelta{Action: basics.SetBytesAction, Uint: 1} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("GlobalState for app %d would use too much space: store bytes count", appIdx)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[creator]) + + // check a happy case + params.GlobalStateSchema = basics.StateSchema{NumUint: 1, NumByteSlice: 1} + br := 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 + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(1, b.put) + pad, ok = b.putBalances[creator] + a.True(ok) + ap, ok = pad.AppParams[appIdx] + a.True(ok) + a.Equal(2, len(ap.GlobalState)) + a.Equal(basics.TealBytesType, ap.GlobalState["bytes"].Type) + a.Equal(basics.TealUintType, ap.GlobalState["uint"].Type) + a.Equal(uint64(0), ap.GlobalState["bytes"].Uint) + a.Equal(uint64(1), ap.GlobalState["uint"].Uint) + a.Equal("", ap.GlobalState["bytes"].Bytes) + a.Equal("", ap.GlobalState["uint"].Bytes) + a.Equal(br, b.balances[creator]) +} + +func TestAppCallApplyLocalsStateDeltas(t *testing.T) { + a := require.New(t) + + var creator basics.Address = getRandomAddress(a) + var sender basics.Address = getRandomAddress(a) + var ac transactions.ApplicationCallTxnFields + var ed basics.EvalDelta + var params basics.AppParams + var appIdx basics.AppIndex + var b testBalances + + b.balances = make(map[basics.Address]basics.AccountData) + ed.LocalDeltas = make(map[uint64]basics.StateDelta) + + err := applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + ed.LocalDeltas[1] = basics.StateDelta{} + + // non-existing account + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + + // empty delta + ac.Accounts = append(ac.Accounts, sender, sender) + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // test duplicates in accounts + b.SetProto(protocol.ConsensusFuture) + ed.LocalDeltas[0] = basics.StateDelta{"uint": basics.ValueDelta{Action: basics.DeleteAction}} + ed.LocalDeltas[1] = basics.StateDelta{"bytes": basics.ValueDelta{Action: basics.DeleteAction}} + b.balances[sender] = basics.AccountData{} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.True(isApplyError(err)) + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{}, b.balances[sender]) + // not opted in + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "acct has not opted in to app") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + // ensure original balance record in the mock was not changed + // this ensure proper cloning and any in-intended in-memory modifications + a.Equal(basics.AccountData{}, b.balances[sender]) + + states := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} + cp := map[basics.AppIndex]basics.AppLocalState{appIdx: {}, 1: {}} + b.balances[sender] = basics.AccountData{ + AppLocalStates: cp, + } + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "duplicate LocalState delta") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + // ensure no changes in original balance record + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + + // test valid deltas and accounts + ac.Accounts = nil + states = map[basics.AppIndex]basics.AppLocalState{appIdx: {}} + b.balances[sender] = basics.AccountData{AppLocalStates: states} + ed.LocalDeltas[0] = basics.StateDelta{ + "uint": basics.ValueDelta{Action: basics.SetUintAction, Uint: 1}, + "bytes": basics.ValueDelta{Action: basics.SetBytesAction, Bytes: "value"}, + } + delete(ed.LocalDeltas, 1) + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.Error(err) + a.Contains(err.Error(), "would use too much space") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + + // happy case + states = map[basics.AppIndex]basics.AppLocalState{appIdx: { + Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }} + cp = map[basics.AppIndex]basics.AppLocalState{appIdx: { + Schema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }} + b.balances[sender] = basics.AccountData{AppLocalStates: cp} + err = applyEvalDelta(&ac, ed, params, creator, sender, &b, appIdx) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + a.Equal(basics.AccountData{AppLocalStates: states}, b.balances[sender]) + a.Equal(basics.TealUintType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Type) + a.Equal(basics.TealBytesType, b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Type) + a.Equal(uint64(1), b.putBalances[sender].AppLocalStates[appIdx].KeyValue["uint"].Uint) + a.Equal("value", b.putBalances[sender].AppLocalStates[appIdx].KeyValue["bytes"].Bytes) +} + +func TestAppCallCreate(t *testing.T) { + a := require.New(t) + + var b testBalances + var txnCounter uint64 = 1 + ac := transactions.ApplicationCallTxnFields{} + creator := getRandomAddress(a) + // no balance record + appIdx, err := createApplication(&ac, &b, creator, txnCounter) + a.Error(err) + a.Equal(basics.AppIndex(0), appIdx) + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + appIdx, err = createApplication(&ac, &b, creator, txnCounter) + a.Error(err) + a.Contains(err.Error(), "max created apps per acct is") + + b.SetProto(protocol.ConsensusFuture) + ac.ApprovalProgram = []byte{1} + ac.ClearStateProgram = []byte{2} + ac.LocalStateSchema = basics.StateSchema{NumUint: 1} + ac.GlobalStateSchema = basics.StateSchema{NumByteSlice: 1} + 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) + nbr, ok := b.putBalances[creator] + a.False(ok) + nbr, ok = b.putWithBalances[creator] + a.True(ok) + params, ok := nbr.AppParams[appIdx] + a.True(ok) + a.Equal(ac.ApprovalProgram, params.ApprovalProgram) + a.Equal(ac.ClearStateProgram, params.ClearStateProgram) + a.Equal(ac.LocalStateSchema, params.LocalStateSchema) + a.Equal(ac.GlobalStateSchema, params.GlobalStateSchema) + a.True(len(b.putWithNew) > 0) +} + +// TestAppCallApplyCreate carefully tracks and validates balance record updates +func TestAppCallApplyCreate(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var txnCounter uint64 = 1 + var b testBalances + + err := ApplicationCall(ac, h, &b, nil, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "cannot use empty ApplyData") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + var ad *transactions.ApplyData = &transactions.ApplyData{} + + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + 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) + + // this test will succeed in creating the app, but then fail + // because the mock balances doesn't update the creators table + // so it will think the app doesn't exist + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "applications that do not exist") + a.Equal(0, b.put) + a.Equal(1, b.putWith) + + createdAppIdx := basics.AppIndex(txnCounter + 1) + b.appCreators = map[basics.AppIndex]basics.Address{createdAppIdx: creator} + + // save the created app info to the side + saved := b.putWithBalances[creator] + + b.ResetWrites() + + // now looking up the creator will succeed, but we reset writes, so + // they won't have the app params + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), fmt.Sprintf("app %d not found in account", createdAppIdx)) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + + b.ResetWrites() + + // now we give the creator the app params again + cp := basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + b.balances[creator] = cp + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + // 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] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.DeltaAction(4), Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "cannot apply GlobalState delta") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + a.Equal(saved, b.balances[creator]) + saved = b.putWithBalances[creator] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + + gd = map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + ac.GlobalStateSchema = basics.StateSchema{NumUint: 1} + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "too much space: store integer") + a.Equal(uint64(steva.appIdx), txnCounter+1) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + a.Equal(saved, b.balances[creator]) + saved = b.putWithBalances[creator] + + b.ResetWrites() + + cp = basics.AccountData{} + cp.AppParams = cloneAppParams(saved.AppParams) + cp.AppLocalStates = cloneAppLocalStates(saved.AppLocalStates) + cp.TotalAppSchema = saved.TotalAppSchema + b.balances[creator] = cp + + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.NoError(err) + appIdx := steva.appIdx + a.Equal(uint64(appIdx), txnCounter+1) + a.Equal(1, b.put) + a.Equal(1, b.putWith) + a.Equal(saved, b.balances[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{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) + br = b.putWithBalances[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) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) +} + +// TestAppCallApplyCreateOptIn checks balance record fields without tracking substages +func TestAppCallApplyCreateOptIn(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + LocalStateSchema: basics.StateSchema{NumByteSlice: 2}, + OnCompletion: transactions.OptInOC, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(steva.appIdx, appIdx) + br := b.balances[creator] + a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) + a.Equal([]byte{1}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumByteSlice: 2}, br.AppLocalStates[appIdx].Schema) + a.Equal(basics.StateSchema{NumUint: 1, NumByteSlice: 2}, br.TotalAppSchema) +} + +func TestAppCallOptIn(t *testing.T) { + a := require.New(t) + + sender := getRandomAddress(a) + + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var b testBalances + ad := basics.AccountData{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + + var params basics.AppParams + + err := applyOptIn(&b, sender, appIdx, params) + 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 = applyOptIn(&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) + + b.ResetWrites() + + ad.AppLocalStates = make(map[basics.AppIndex]basics.AppLocalState) + ad.AppLocalStates[appIdx] = basics.AppLocalState{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&b, sender, appIdx, params) + 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() + + delete(ad.AppLocalStates, appIdx) + ad.AppLocalStates[appIdx+1] = basics.AppLocalState{} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&b, sender, appIdx, params) + a.NoError(err) + a.Equal(1, b.put) + a.Equal(0, b.putWith) + + b.ResetWrites() + + ad.AppLocalStates[appIdx+1] = basics.AppLocalState{ + Schema: basics.StateSchema{NumByteSlice: 1}, + } + ad.TotalAppSchema = basics.StateSchema{NumByteSlice: 1} + params.LocalStateSchema = basics.StateSchema{NumUint: 1} + b.balances = map[basics.Address]basics.AccountData{sender: ad} + err = applyOptIn(&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: {Schema: basics.StateSchema{NumUint: 1}}, + appIdx + 1: {Schema: basics.StateSchema{NumByteSlice: 1}}, + }, + TotalAppSchema: basics.StateSchema{NumUint: 1, NumByteSlice: 1}, + }, + br, + ) +} + +func TestAppCallClearState(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + ac := transactions.ApplicationCallTxnFields{} + var steva testEvaluator + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var b testBalances + + ad := &transactions.ApplyData{} + b.appCreators = make(map[basics.AppIndex]basics.Address) + b.balances = make(map[basics.Address]basics.AccountData, 2) + b.SetProto(protocol.ConsensusFuture) + + // check app not exist and not opted in + b.balances[sender] = basics.AccountData{} + err := applyClearState(&ac, &b, sender, appIdx, ad, &steva) + a.Error(err) + a.Contains(err.Error(), "not currently opted in") + a.Equal(0, b.put) + a.Equal(0, b.putWith) + + // check non-existing app with empty opt-in + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, + } + err = applyClearState(&ac, &b, sender, appIdx, ad, &steva) + 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) + // check original balance record not changed + br = b.balances[sender] + a.Equal(map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, br.AppLocalStates) + + b.ResetWrites() + + // check non-existing app with non-empty opt-in + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{ + appIdx: {Schema: basics.StateSchema{NumUint: 10}}, + }, + } + err = applyClearState(&ac, &b, sender, appIdx, ad, &steva) + 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) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + + b.ResetWrites() + + // check existing application with failing ClearStateProgram + b.balances[creator] = basics.AccountData{ + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdx: { + ClearStateProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + }, + }, + } + b.appCreators[appIdx] = creator + + // one put: to opt out + steva.pass = false + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + err = applyClearState(&ac, &b, sender, appIdx, ad, &steva) + 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) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + + b.ResetWrites() + + // check existing application with successful ClearStateProgram. two + // puts: one to write global state, one to opt out + steva.pass = true + err = applyClearState(&ac, &b, sender, appIdx, ad, &steva) + a.NoError(err) + a.Equal(2, b.put) + a.Equal(0, b.putWith) + a.Equal(0, len(br.AppLocalStates)) + a.Equal(basics.StateSchema{}, br.TotalAppSchema) + a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) +} + +func TestAppCallApplyCloseOut(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.CloseOutOC, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 closing on empty sender's balance record + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + 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) + + b.ResetWrites() + + // check a happy case + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + b.balances[sender] = basics.AccountData{ + AppLocalStates: map[basics.AppIndex]basics.AppLocalState{appIdx: {}}, + } + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(2, b.put) + a.Equal(0, b.putWith) + br = b.putBalances[creator] + a.NotEqual(cbr, br) + a.Equal(basics.TealKeyValue{"uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}}, br.AppParams[appIdx].GlobalState) + br = b.putBalances[sender] + 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) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.UpdateApplicationOC, + ApprovalProgram: []byte{2}, + ClearStateProgram: []byte{3}, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 updating on empty sender's balance record - happy case + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + 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{3}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) +} + +func TestAppCallApplyDelete(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := getRandomAddress(a) + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: appIdx, + OnCompletion: transactions.DeleteApplicationOC, + } + params := basics.AppParams{ + ApprovalProgram: []byte{1}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + }, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalances + + 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} + + b.SetProto(protocol.ConsensusFuture) + + steva.pass = false + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "transaction rejected by ApprovalProgram") + a.Equal(steva.appIdx, appIdx) + 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 + steva.pass = true + b.balances[sender] = basics.AccountData{} + err = ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(0, b.put) + a.Equal(1, b.putWith) + 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) +} + +func TestAppCallApplyCreateClearState(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{2}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + OnCompletion: transactions.ClearStateOC, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + // check creation on empty balance record + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.Error(err) + a.Contains(err.Error(), "not currently opted in") + a.Equal(steva.appIdx, appIdx) + a.Equal(basics.EvalDelta{}, ad.EvalDelta) + br := b.balances[creator] + a.Equal([]byte{1}, br.AppParams[appIdx].ApprovalProgram) + a.Equal([]byte{2}, br.AppParams[appIdx].ClearStateProgram) + a.Equal(basics.StateSchema{NumUint: 1}, br.AppParams[appIdx].GlobalStateSchema) + a.Equal(basics.StateSchema{}, br.AppParams[appIdx].LocalStateSchema) + a.Equal(basics.StateSchema{NumUint: 1}, br.TotalAppSchema) + a.Equal(basics.TealKeyValue(nil), br.AppParams[appIdx].GlobalState) +} + +func TestAppCallApplyCreateDelete(t *testing.T) { + a := require.New(t) + + creator := getRandomAddress(a) + sender := creator + ac := transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: []byte{1}, + ClearStateProgram: []byte{1}, + GlobalStateSchema: basics.StateSchema{NumUint: 1}, + OnCompletion: transactions.DeleteApplicationOC, + } + h := transactions.Header{ + Sender: sender, + } + var steva testEvaluator + var txnCounter uint64 = 1 + appIdx := basics.AppIndex(txnCounter + 1) + var ad *transactions.ApplyData = &transactions.ApplyData{} + var b testBalancesPass + + b.balances = make(map[basics.Address]basics.AccountData) + b.balances[creator] = basics.AccountData{} + b.SetProto(protocol.ConsensusFuture) + b.appCreators = map[basics.AppIndex]basics.Address{appIdx: creator} + + steva.pass = true + gd := map[string]basics.ValueDelta{"uint": {Action: basics.SetUintAction, Uint: 1}} + steva.delta = basics.EvalDelta{GlobalDelta: gd} + + // check creation on empty balance record + err := ApplicationCall(ac, h, &b, ad, txnCounter, &steva) + a.NoError(err) + a.Equal(steva.appIdx, appIdx) + a.Equal(basics.EvalDelta{GlobalDelta: gd}, ad.EvalDelta) + br := b.balances[creator] + a.Equal(basics.AppParams{}, br.AppParams[appIdx]) +} diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go new file mode 100644 index 0000000000..b72986409f --- /dev/null +++ b/ledger/apply/apply.go @@ -0,0 +1,41 @@ +package apply + +import ( + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" +) + +// Balances allow to move MicroAlgos from one address to another and to update balance records, or to access and modify individual balance records +// After a call to Put (or Move), future calls to Get or Move will reflect the updated balance record(s) +type Balances interface { + // Get looks up the balance record for an address + // If the account is known to be empty, then err should be nil and the returned balance record should have the given address and empty AccountData + // withPendingRewards specifies whether pending rewards should be applied. + // A non-nil error means the lookup is impossible (e.g., if the database doesn't have necessary state anymore) + Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) + + Put(basics.BalanceRecord) error + + // PutWithCreatable is like Put, but should be used when creating or deleting an asset or application. + PutWithCreatable(record basics.BalanceRecord, 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) + + // Move MicroAlgos from one account to another, doing all necessary overflow checking (convenience method) + // TODO: Does this need to be part of the balances interface, or can it just be implemented here as a function that calls Put and Get? + Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards *basics.MicroAlgos, dstRewards *basics.MicroAlgos) error + + // Balances correspond to a Round, which mean that they also correspond + // to a ConsensusParams. This returns those parameters. + ConsensusParams() config.ConsensusParams +} + +// StateEvaluator is an interface that provides some Stateful TEAL +// functionality that may be passed through to Apply from ledger, avoiding a +// circular dependency between the logic and transactions packages +type StateEvaluator interface { + Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) + Check(program []byte) (cost int, err error) + InitLedger(balances Balances, appIdx basics.AppIndex, schemas basics.StateSchemas) error +} diff --git a/ledger/apply/asset.go b/ledger/apply/asset.go new file mode 100644 index 0000000000..822eea559f --- /dev/null +++ b/ledger/apply/asset.go @@ -0,0 +1,394 @@ +// 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 . + +package apply + +import ( + "fmt" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" +) + +func cloneAssetHoldings(m map[basics.AssetIndex]basics.AssetHolding) map[basics.AssetIndex]basics.AssetHolding { + res := make(map[basics.AssetIndex]basics.AssetHolding) + for id, val := range m { + res[id] = val + } + return res +} + +func cloneAssetParams(m map[basics.AssetIndex]basics.AssetParams) map[basics.AssetIndex]basics.AssetParams { + res := make(map[basics.AssetIndex]basics.AssetParams) + for id, val := range m { + res[id] = val + } + return res +} + +func getParams(balances Balances, aidx basics.AssetIndex) (params basics.AssetParams, creator basics.Address, err error) { + creator, exists, err := balances.GetCreator(basics.CreatableIndex(aidx), basics.AssetCreatable) + if err != nil { + return + } + + // For assets, anywhere we're attempting to fetch parameters, we are + // assuming that the asset should exist. + if !exists { + err = fmt.Errorf("asset %d does not exist or has been deleted", aidx) + return + } + + creatorRecord, err := balances.Get(creator, false) + if err != nil { + return + } + + params, ok := creatorRecord.AssetParams[aidx] + if !ok { + err = fmt.Errorf("asset index %d not found in account %s", aidx, creator.String()) + return + } + + return +} + +// AssetConfig applies an AssetConfig transaction using the Balances interface. +func AssetConfig(cc transactions.AssetConfigTxnFields, header transactions.Header, balances Balances, spec transactions.SpecialAddresses, ad *transactions.ApplyData, txnCounter uint64) error { + if cc.ConfigAsset == 0 { + // Allocating an asset. + record, err := balances.Get(header.Sender, false) + if err != nil { + return err + } + record.Assets = cloneAssetHoldings(record.Assets) + record.AssetParams = cloneAssetParams(record.AssetParams) + + // Ensure index is never zero + newidx := basics.AssetIndex(txnCounter + 1) + + // Sanity check that there isn't an asset with this counter value. + _, present := record.AssetParams[newidx] + if present { + return fmt.Errorf("already found asset with index %d", newidx) + } + + record.AssetParams[newidx] = cc.AssetParams + record.Assets[newidx] = basics.AssetHolding{ + Amount: cc.AssetParams.Total, + } + + if len(record.Assets) > balances.ConsensusParams().MaxAssetsPerAccount { + 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), + } + + return balances.PutWithCreatable(record, created, nil) + } + + // Re-configuration and destroying must be done by the manager key. + params, creator, err := getParams(balances, cc.ConfigAsset) + if err != nil { + return err + } + + if params.Manager.IsZero() || (header.Sender != params.Manager) { + return fmt.Errorf("this transaction should be issued by the manager. It is issued by %v, manager key %v", header.Sender, params.Manager) + } + + record, err := balances.Get(creator, false) + if err != nil { + return err + } + + 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. + if record.Assets[cc.ConfigAsset].Amount != params.Total { + return fmt.Errorf("cannot destroy asset: creator is holding only %d/%d", record.Assets[cc.ConfigAsset].Amount, params.Total) + } + + // Tell the cow what asset we deleted + deleted = &basics.CreatableLocator{ + Creator: creator, + Type: basics.AssetCreatable, + Index: basics.CreatableIndex(cc.ConfigAsset), + } + + delete(record.Assets, cc.ConfigAsset) + delete(record.AssetParams, cc.ConfigAsset) + } else { + // Changing keys in an asset. + if !params.Manager.IsZero() { + params.Manager = cc.AssetParams.Manager + } + if !params.Reserve.IsZero() { + params.Reserve = cc.AssetParams.Reserve + } + if !params.Freeze.IsZero() { + params.Freeze = cc.AssetParams.Freeze + } + if !params.Clawback.IsZero() { + params.Clawback = cc.AssetParams.Clawback + } + + record.AssetParams[cc.ConfigAsset] = params + } + + return balances.PutWithCreatable(record, nil, deleted) +} + +func takeOut(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { + if amount == 0 { + return nil + } + + snd, err := balances.Get(addr, false) + if err != nil { + return err + } + + snd.Assets = cloneAssetHoldings(snd.Assets) + sndHolding, ok := snd.Assets[asset] + if !ok { + return fmt.Errorf("asset %v missing from %v", asset, addr) + } + + if sndHolding.Frozen && !bypassFreeze { + return fmt.Errorf("asset %v frozen in %v", asset, addr) + } + + newAmount, overflowed := basics.OSub(sndHolding.Amount, amount) + if overflowed { + return fmt.Errorf("underflow on subtracting %d from sender amount %d", amount, sndHolding.Amount) + } + sndHolding.Amount = newAmount + + snd.Assets[asset] = sndHolding + return balances.Put(snd) +} + +func putIn(balances Balances, addr basics.Address, asset basics.AssetIndex, amount uint64, bypassFreeze bool) error { + if amount == 0 { + return nil + } + + rcv, err := balances.Get(addr, false) + if err != nil { + return err + } + + rcv.Assets = cloneAssetHoldings(rcv.Assets) + rcvHolding, ok := rcv.Assets[asset] + if !ok { + return fmt.Errorf("asset %v missing from %v", asset, addr) + } + + if rcvHolding.Frozen && !bypassFreeze { + return fmt.Errorf("asset frozen in recipient") + } + + var overflowed bool + rcvHolding.Amount, overflowed = basics.OAdd(rcvHolding.Amount, amount) + if overflowed { + return fmt.Errorf("overflow on adding %d to receiver amount %d", amount, rcvHolding.Amount) + } + + rcv.Assets[asset] = rcvHolding + return balances.Put(rcv) +} + +// AssetTransfer applies an AssetTransfer transaction using the Balances interface. +func AssetTransfer(ct transactions.AssetTransferTxnFields, header transactions.Header, balances Balances, spec transactions.SpecialAddresses, ad *transactions.ApplyData) error { + // Default to sending from the transaction sender's account. + source := header.Sender + clawback := false + + if !ct.AssetSender.IsZero() { + // Clawback transaction. Check that the transaction sender + // is the Clawback address for this asset. + params, _, err := getParams(balances, ct.XferAsset) + if err != nil { + return err + } + + if params.Clawback.IsZero() || (header.Sender != params.Clawback) { + return fmt.Errorf("clawback not allowed: sender %v, clawback %v", header.Sender, params.Clawback) + } + + // Transaction sent from the correct clawback address, + // execute asset transfer from specified source. + source = ct.AssetSender + clawback = true + } + + // Allocate a slot for asset (self-transfer of zero amount). + if ct.AssetAmount == 0 && ct.AssetReceiver == source && !clawback { + snd, err := balances.Get(source, false) + if err != nil { + return err + } + + snd.Assets = cloneAssetHoldings(snd.Assets) + sndHolding, ok := snd.Assets[ct.XferAsset] + if !ok { + // Initialize holding with default Frozen value. + params, _, err := getParams(balances, ct.XferAsset) + if err != nil { + return err + } + + sndHolding.Frozen = params.DefaultFrozen + snd.Assets[ct.XferAsset] = sndHolding + + if len(snd.Assets) > balances.ConsensusParams().MaxAssetsPerAccount { + return fmt.Errorf("too many assets in account: %d > %d", len(snd.Assets), balances.ConsensusParams().MaxAssetsPerAccount) + } + + err = balances.Put(snd) + if err != nil { + return err + } + } + } + + // Actually move the asset. Zero transfers return right away + // without looking up accounts, so it's fine to have a zero transfer + // to an all-zero address (e.g., when the only meaningful part of + // the transaction is the close-to address). Similarly, takeOut and + // putIn will succeed for zero transfers on frozen asset holdings + err := takeOut(balances, source, ct.XferAsset, ct.AssetAmount, clawback) + if err != nil { + return err + } + + err = putIn(balances, ct.AssetReceiver, ct.XferAsset, ct.AssetAmount, clawback) + if err != nil { + return err + } + + if ct.AssetCloseTo != (basics.Address{}) { + // Cannot close by clawback + if clawback { + return fmt.Errorf("cannot close asset by clawback") + } + + // Fetch the sender balance record. We will use this to ensure + // that the sender is not the creator of the asset, and to + // figure out how much of the asset to move. + snd, err := balances.Get(source, false) + if err != nil { + return err + } + + // The creator of the asset cannot close their holding of the + // asset. Check if we are the creator by seeing if there is an + // AssetParams entry for the asset index. + if _, ok := snd.AssetParams[ct.XferAsset]; ok { + return fmt.Errorf("cannot close asset ID in allocating account") + } + + // Fetch our asset holding, which should exist since we're + // closing it out + sndHolding, ok := snd.Assets[ct.XferAsset] + if !ok { + return fmt.Errorf("asset %v not present in account %v", ct.XferAsset, source) + } + + // Fetch the destination balance record to check if we are + // closing out to the creator + dst, err := balances.Get(ct.AssetCloseTo, false) + if err != nil { + return err + } + + // Allow closing out to the asset creator even when frozen. + // If we are closing out 0 units of the asset, then takeOut + // and putIn will short circuit (so bypassFreeze doesn't matter) + _, bypassFreeze := dst.AssetParams[ct.XferAsset] + + // Move the balance out. + err = takeOut(balances, source, ct.XferAsset, sndHolding.Amount, bypassFreeze) + if err != nil { + return err + } + + // Put the balance in. + err = putIn(balances, ct.AssetCloseTo, ct.XferAsset, sndHolding.Amount, bypassFreeze) + if err != nil { + return err + } + + // Delete the slot from the account. + snd, err = balances.Get(source, false) + if err != nil { + return err + } + + snd.Assets = cloneAssetHoldings(snd.Assets) + sndHolding = snd.Assets[ct.XferAsset] + if sndHolding.Amount != 0 { + return fmt.Errorf("asset %v not zero (%d) after closing", ct.XferAsset, sndHolding.Amount) + } + + delete(snd.Assets, ct.XferAsset) + err = balances.Put(snd) + if err != nil { + return err + } + } + + return nil +} + +// AssetFreeze applies an AssetFreeze transaction using the Balances interface. +func AssetFreeze(cf transactions.AssetFreezeTxnFields, header transactions.Header, balances Balances, spec transactions.SpecialAddresses, ad *transactions.ApplyData) error { + // Only the Freeze address can change the freeze value. + params, _, err := getParams(balances, cf.FreezeAsset) + if err != nil { + return err + } + + if params.Freeze.IsZero() || (header.Sender != params.Freeze) { + return fmt.Errorf("freeze not allowed: sender %v, freeze %v", header.Sender, params.Freeze) + } + + // Get the account to be frozen/unfrozen. + record, err := balances.Get(cf.FreezeAccount, false) + if err != nil { + return err + } + record.Assets = cloneAssetHoldings(record.Assets) + + holding, ok := record.Assets[cf.FreezeAsset] + if !ok { + return fmt.Errorf("asset not found in account") + } + + holding.Frozen = cf.AssetFrozen + record.Assets[cf.FreezeAsset] = holding + return balances.Put(record) +} diff --git a/ledger/apply/keyreg.go b/ledger/apply/keyreg.go new file mode 100644 index 0000000000..aef9b8905f --- /dev/null +++ b/ledger/apply/keyreg.go @@ -0,0 +1,75 @@ +// 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 . + +package apply + +import ( + "fmt" + + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" +) + +// Keyreg applies a KeyRegistration transaction using the Balances interface. +func Keyreg(keyreg transactions.KeyregTxnFields, header transactions.Header, balances Balances, spec transactions.SpecialAddresses, ad *transactions.ApplyData) error { + if header.Sender == spec.FeeSink { + return fmt.Errorf("cannot register participation key for fee sink's address %v ", header.Sender) + } + + // Get the user's balance entry + record, err := balances.Get(header.Sender, false) + if err != nil { + return err + } + + // non-participatory accounts cannot be brought online (or offline) + if record.Status == basics.NotParticipating { + return fmt.Errorf("cannot change online/offline status of non-participating account %v", header.Sender) + } + + // Update the registered keys and mark account as online + // (or, if the voting or selection keys are zero, offline/not-participating) + record.VoteID = keyreg.VotePK + record.SelectionID = keyreg.SelectionPK + if (keyreg.VotePK == crypto.OneTimeSignatureVerifier{} || keyreg.SelectionPK == crypto.VRFVerifier{}) { + if keyreg.Nonparticipation { + if balances.ConsensusParams().SupportBecomeNonParticipatingTransactions { + record.Status = basics.NotParticipating + } else { + return fmt.Errorf("transaction tries to mark an account as nonparticipating, but that transaction is not supported") + } + } else { + record.Status = basics.Offline + } + record.VoteFirstValid = 0 + record.VoteLastValid = 0 + record.VoteKeyDilution = 0 + } else { + record.Status = basics.Online + record.VoteFirstValid = keyreg.VoteFirst + record.VoteLastValid = keyreg.VoteLast + record.VoteKeyDilution = keyreg.VoteKeyDilution + } + + // Write the updated entry + err = balances.Put(record) + if err != nil { + return err + } + + return nil +} diff --git a/data/transactions/keyreg_test.go b/ledger/apply/keyreg_test.go similarity index 81% rename from data/transactions/keyreg_test.go rename to ledger/apply/keyreg_test.go index 1adb5038a3..aaa9cd6dc2 100644 --- a/data/transactions/keyreg_test.go +++ b/ledger/apply/keyreg_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package transactions +package apply import ( "testing" @@ -22,6 +22,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/protocol" "github.com/stretchr/testify/require" ) @@ -68,24 +69,24 @@ func TestKeyregApply(t *testing.T) { vrfSecrets := crypto.GenerateVRFSecrets() secretParticipation := keypair() - tx := Transaction{ + tx := transactions.Transaction{ Type: protocol.KeyRegistrationTx, - Header: Header{ + Header: transactions.Header{ Sender: src, Fee: basics.MicroAlgos{Raw: 1}, FirstValid: basics.Round(100), LastValid: basics.Round(1000), }, - KeyregTxnFields: KeyregTxnFields{ + KeyregTxnFields: transactions.KeyregTxnFields{ VotePK: crypto.OneTimeSignatureVerifier(secretParticipation.SignatureVerifier), SelectionPK: vrfSecrets.PK, }, } - _, err := tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, nil, SpecialAddresses{FeeSink: feeSink}, 0) + err := Keyreg(tx.KeyregTxnFields, tx.Header, mockBalances{protocol.ConsensusCurrentVersion}, transactions.SpecialAddresses{FeeSink: feeSink}, nil) require.NoError(t, err) tx.Sender = feeSink - _, err = tx.Apply(mockBalances{protocol.ConsensusCurrentVersion}, nil, SpecialAddresses{FeeSink: feeSink}, 0) + err = Keyreg(tx.KeyregTxnFields, tx.Header, mockBalances{protocol.ConsensusCurrentVersion}, transactions.SpecialAddresses{FeeSink: feeSink}, nil) require.Error(t, err) tx.Sender = src @@ -94,19 +95,19 @@ func TestKeyregApply(t *testing.T) { // Going from offline to online should be okay mockBal.addrs[src] = basics.BalanceRecord{Addr: src, AccountData: basics.AccountData{Status: basics.Offline}} - _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) + err = Keyreg(tx.KeyregTxnFields, tx.Header, mockBal, transactions.SpecialAddresses{FeeSink: feeSink}, nil) require.NoError(t, err) // Going from online to nonparticipatory should be okay, if the protocol supports that if mockBal.ConsensusParams().SupportBecomeNonParticipatingTransactions { - tx.KeyregTxnFields = KeyregTxnFields{} + tx.KeyregTxnFields = transactions.KeyregTxnFields{} tx.KeyregTxnFields.Nonparticipation = true - _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) + err = Keyreg(tx.KeyregTxnFields, tx.Header, mockBal, transactions.SpecialAddresses{FeeSink: feeSink}, nil) require.NoError(t, err) // Nonparticipatory accounts should not be able to change status mockBal.addrs[src] = basics.BalanceRecord{Addr: src, AccountData: basics.AccountData{Status: basics.NotParticipating}} - _, err = tx.Apply(mockBal, nil, SpecialAddresses{FeeSink: feeSink}, 0) + err = Keyreg(tx.KeyregTxnFields, tx.Header, mockBal, transactions.SpecialAddresses{FeeSink: feeSink}, nil) require.Error(t, err) } } diff --git a/ledger/apply/payment.go b/ledger/apply/payment.go new file mode 100644 index 0000000000..a34d93fd14 --- /dev/null +++ b/ledger/apply/payment.go @@ -0,0 +1,110 @@ +// 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 . + +package apply + +import ( + "fmt" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" +) + +func checkSpender(payment transactions.PaymentTxnFields, header transactions.Header, spec transactions.SpecialAddresses, proto config.ConsensusParams) error { + if header.Sender == payment.CloseRemainderTo { + return fmt.Errorf("transaction cannot close account to its sender %v", header.Sender) + } + + // the FeeSink account may only spend to the IncentivePool + if header.Sender == spec.FeeSink { + if payment.Receiver != spec.RewardsPool { + return fmt.Errorf("cannot spend from fee sink's address %v to non incentive pool address %v", header.Sender, payment.Receiver) + } + if payment.CloseRemainderTo != (basics.Address{}) { + return fmt.Errorf("cannot close fee sink %v to %v", header.Sender, payment.CloseRemainderTo) + } + } + return nil +} + +// Payment changes the balances according to this transaction. +// The ApplyData argument should reflect the changes made by +// apply(). It may already include changes made by the caller +// (i.e., Transaction.Apply), so apply() must update it rather +// than overwriting it. For example, Transaction.Apply() may +// have updated ad.SenderRewards, and this function should only +// add to ad.SenderRewards (if needed), but not overwrite it. +func Payment(payment transactions.PaymentTxnFields, header transactions.Header, balances Balances, spec transactions.SpecialAddresses, ad *transactions.ApplyData) error { + // move tx money + if !payment.Amount.IsZero() || payment.Receiver != (basics.Address{}) { + err := balances.Move(header.Sender, payment.Receiver, payment.Amount, &ad.SenderRewards, &ad.ReceiverRewards) + if err != nil { + return err + } + } + + if payment.CloseRemainderTo != (basics.Address{}) { + rec, err := balances.Get(header.Sender, true) + if err != nil { + return err + } + + closeAmount := rec.AccountData.MicroAlgos + ad.ClosingAmount = closeAmount + err = balances.Move(header.Sender, payment.CloseRemainderTo, closeAmount, &ad.SenderRewards, &ad.CloseRewards) + if err != nil { + return err + } + + // Confirm that we have no balance left + rec, err = balances.Get(header.Sender, true) + if !rec.AccountData.MicroAlgos.IsZero() { + return fmt.Errorf("balance %d still not zero after CloseRemainderTo", rec.AccountData.MicroAlgos.Raw) + } + + // Confirm that there is no asset-related state in the account + if len(rec.Assets) > 0 { + return fmt.Errorf("cannot close: %d outstanding assets", len(rec.Assets)) + } + + if len(rec.AssetParams) > 0 { + // This should be impossible because every asset created + // by an account (in AssetParams) must also appear in Assets, + // which we checked above. + return fmt.Errorf("cannot close: %d outstanding created assets", len(rec.AssetParams)) + } + + // Confirm that there is no application-related state remaining + if len(rec.AppLocalStates) > 0 { + return fmt.Errorf("cannot close: %d outstanding applications opted in. Please opt out or clear them", len(rec.AppLocalStates)) + } + + // Can't have created apps remaining either + if len(rec.AppParams) > 0 { + return fmt.Errorf("cannot close: %d outstanding created applications", len(rec.AppParams)) + } + + // Clear out entire account record, to allow the DB to GC it + rec.AccountData = basics.AccountData{} + err = balances.Put(rec) + if err != nil { + return err + } + } + + return nil +} diff --git a/ledger/apply/payment_test.go b/ledger/apply/payment_test.go new file mode 100644 index 0000000000..59f74336eb --- /dev/null +++ b/ledger/apply/payment_test.go @@ -0,0 +1,406 @@ +// 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 . + +package apply + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/crypto" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/protocol" +) + +var poolAddr = basics.Address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + +var spec = transactions.SpecialAddresses{ + FeeSink: feeSink, + RewardsPool: poolAddr, +} + +func keypair() *crypto.SignatureSecrets { + var seed crypto.Seed + crypto.RandBytes(seed[:]) + s := crypto.GenerateSignatureSecrets(seed) + return s +} + +func TestAlgosEncoding(t *testing.T) { + var a basics.MicroAlgos + var b basics.MicroAlgos + var i uint64 + + a.Raw = 222233333 + err := protocol.Decode(protocol.Encode(&a), &b) + if err != nil { + panic(err) + } + require.Equal(t, a, b) + + a.Raw = 12345678 + err = protocol.DecodeReflect(protocol.Encode(a), &i) + if err != nil { + panic(err) + } + require.Equal(t, a.Raw, i) + + i = 87654321 + err = protocol.Decode(protocol.EncodeReflect(i), &a) + if err != nil { + panic(err) + } + require.Equal(t, a.Raw, i) + + x := true + err = protocol.Decode(protocol.EncodeReflect(x), &a) + if err == nil { + panic("decode of bool into MicroAlgos succeeded") + } +} + +type mockBalances struct { + protocol.ConsensusVersion +} + +func (balances mockBalances) Round() basics.Round { + return basics.Round(8675309) +} + +func (balances mockBalances) PutWithCreatable(basics.BalanceRecord, *basics.CreatableLocator, *basics.CreatableLocator) error { + return nil +} + +func (balances mockBalances) Get(basics.Address, bool) (basics.BalanceRecord, error) { + return basics.BalanceRecord{}, nil +} + +func (balances mockBalances) GetCreator(idx basics.CreatableIndex, ctype basics.CreatableType) (basics.Address, bool, error) { + return basics.Address{}, true, nil +} + +func (balances mockBalances) Put(basics.BalanceRecord) error { + return nil +} + +func (balances mockBalances) Move(src, dst basics.Address, amount basics.MicroAlgos, srcRewards, dstRewards *basics.MicroAlgos) error { + return nil +} + +func (balances mockBalances) ConsensusParams() config.ConsensusParams { + return config.Consensus[balances.ConsensusVersion] +} + +func TestPaymentApply(t *testing.T) { + mockBalV0 := mockBalances{protocol.ConsensusCurrentVersion} + + secretSrc := keypair() + src := basics.Address(secretSrc.SignatureVerifier) + + secretDst := keypair() + dst := basics.Address(secretDst.SignatureVerifier) + + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: src, + Fee: basics.MicroAlgos{Raw: 1}, + FirstValid: basics.Round(100), + LastValid: basics.Round(1000), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: dst, + Amount: basics.MicroAlgos{Raw: uint64(50)}, + }, + } + var ad transactions.ApplyData + err := Payment(tx.PaymentTxnFields, tx.Header, mockBalV0, transactions.SpecialAddresses{FeeSink: feeSink}, &ad) + require.NoError(t, err) +} + +func TestCheckSpender(t *testing.T) { + mockBalV0 := mockBalances{protocol.ConsensusCurrentVersion} + mockBalV7 := mockBalances{protocol.ConsensusV7} + + secretSrc := keypair() + src := basics.Address(secretSrc.SignatureVerifier) + + secretDst := keypair() + dst := basics.Address(secretDst.SignatureVerifier) + + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: src, + Fee: basics.MicroAlgos{Raw: 1}, + FirstValid: basics.Round(100), + LastValid: basics.Round(1000), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: dst, + Amount: basics.MicroAlgos{Raw: uint64(50)}, + }, + } + + tx.Sender = basics.Address(feeSink) + require.Error(t, checkSpender(tx.PaymentTxnFields, tx.Header, spec, mockBalV0.ConsensusParams())) + + poolAddr := basics.Address(poolAddr) + tx.Receiver = poolAddr + require.NoError(t, checkSpender(tx.PaymentTxnFields, tx.Header, spec, mockBalV0.ConsensusParams())) + + tx.CloseRemainderTo = poolAddr + require.Error(t, checkSpender(tx.PaymentTxnFields, tx.Header, spec, mockBalV0.ConsensusParams())) + require.Error(t, checkSpender(tx.PaymentTxnFields, tx.Header, spec, mockBalV7.ConsensusParams())) + + tx.Sender = src + require.NoError(t, checkSpender(tx.PaymentTxnFields, tx.Header, spec, mockBalV7.ConsensusParams())) +} + +func TestPaymentValidation(t *testing.T) { + payments, _, _, _ := generateTestObjects(100, 50) + genHash := crypto.Digest{0x42} + for i, txn := range payments { + txn.GenesisHash = genHash + payments[i] = txn + } + tc := transactions.ExplicitTxnContext{ + Proto: config.Consensus[protocol.ConsensusCurrentVersion], + GenHash: genHash, + } + for _, txn := range payments { + // Lifetime window + tc.ExplicitRound = txn.First() + 1 + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive during lifetime %v", txn) + } + + tc.ExplicitRound = txn.First() + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive at issuance %v", txn) + } + + tc.ExplicitRound = txn.Last() + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive at expiry %v", txn) + } + + tc.ExplicitRound = txn.First() - 1 + if txn.Alive(tc) == nil { + t.Errorf("premature transaction alive %v", txn) + } + + tc.ExplicitRound = txn.Last() + 1 + if txn.Alive(tc) == nil { + t.Errorf("expired transaction alive %v", txn) + } + + // Make a copy of txn, change some fields, be sure the TXID changes. This is not exhaustive. + var txn2 transactions.Transaction + txn2 = txn + txn2.Note = []byte{42} + if txn2.ID() == txn.ID() { + t.Errorf("txid does not depend on note") + } + txn2 = txn + txn2.Amount.Raw++ + if txn2.ID() == txn.ID() { + t.Errorf("txid does not depend on amount") + } + txn2 = txn + txn2.Fee.Raw++ + if txn2.ID() == txn.ID() { + t.Errorf("txid does not depend on fee") + } + txn2 = txn + txn2.LastValid++ + if txn2.ID() == txn.ID() { + t.Errorf("txid does not depend on lastvalid") + } + + // Check malformed transactions + largeWindow := txn + largeWindow.LastValid += basics.Round(tc.Proto.MaxTxnLife) + if largeWindow.WellFormed(spec, tc.Proto) == nil { + t.Errorf("transaction with large window %#v verified incorrectly", largeWindow) + } + + badWindow := txn + badWindow.LastValid = badWindow.FirstValid - 1 + if badWindow.WellFormed(spec, tc.Proto) == nil { + t.Errorf("transaction with bad window %#v verified incorrectly", badWindow) + } + + badFee := txn + badFee.Fee = basics.MicroAlgos{} + if badFee.WellFormed(spec, tc.Proto) == nil { + t.Errorf("transaction with no fee %#v verified incorrectly", badFee) + } + } +} + +func TestPaymentSelfClose(t *testing.T) { + secretSrc := keypair() + src := basics.Address(secretSrc.SignatureVerifier) + + secretDst := keypair() + dst := basics.Address(secretDst.SignatureVerifier) + + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: src, + Fee: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinTxnFee}, + FirstValid: basics.Round(100), + LastValid: basics.Round(1000), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: dst, + Amount: basics.MicroAlgos{Raw: uint64(50)}, + CloseRemainderTo: src, + }, + } + require.Error(t, tx.WellFormed(spec, config.Consensus[protocol.ConsensusCurrentVersion])) +} + +func generateTestObjects(numTxs, numAccs int) ([]transactions.Transaction, []transactions.SignedTxn, []*crypto.SignatureSecrets, []basics.Address) { + txs := make([]transactions.Transaction, numTxs) + signed := make([]transactions.SignedTxn, numTxs) + secrets := make([]*crypto.SignatureSecrets, numAccs) + addresses := make([]basics.Address, numAccs) + + for i := 0; i < numAccs; i++ { + secret := keypair() + addr := basics.Address(secret.SignatureVerifier) + secrets[i] = secret + addresses[i] = addr + } + + for i := 0; i < numTxs; i++ { + s := rand.Intn(numAccs) + r := rand.Intn(numAccs) + a := rand.Intn(1000) + f := config.Consensus[protocol.ConsensusCurrentVersion].MinTxnFee + uint64(rand.Intn(10)) + iss := 50 + rand.Intn(30) + exp := iss + 10 + + txs[i] = transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: addresses[s], + Fee: basics.MicroAlgos{Raw: f}, + FirstValid: basics.Round(iss), + LastValid: basics.Round(exp), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: addresses[r], + Amount: basics.MicroAlgos{Raw: uint64(a)}, + }, + } + signed[i] = txs[i].Sign(secrets[s]) + } + + return txs, signed, secrets, addresses +} + +/* +func TestTxnValidation(t *testing.T) { + _, signed, _, _ := generateTestObjects(100, 50) + tc := ExplicitTxnContext{ + Proto: config.Consensus[protocol.ConsensusCurrentVersion], + } + + for i, stxn := range signed { + if stxn.Verify() != nil { + t.Errorf("signed transaction %#v did not verify", stxn) + } + txn := stxn.Transaction.(Payment) + + tc.ExplicitRound = txn.First()+1 + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive during lifetime %v", txn) + } + + tc.ExplicitRound = txn.First() + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive at issuance %v", txn) + } + + tc.ExplicitRound = txn.Last() + if txn.Alive(tc) != nil { + t.Errorf("transaction not alive at expiry %v", txn) + } + + tc.ExplicitRound = txn.First()-1 + if txn.Alive(tc) != nil { + t.Errorf("premature transaction alive %v", txn) + } + + tc.ExplicitRound = txn.Last()+1 + if txn.Alive(tc) != nil { + t.Errorf("expired transaction alive %v", txn) + } + + badSig := stxn + otherTransaction := txn + otherTransaction.Note = []byte{42} + badSig.Transaction = &otherTransaction + badSig.InitCaches() + if badSig.Verify() == nil { + t.Errorf("modified transaction %#v verified incorrectly", badSig) + } + + noSig := stxn + noSig.Sig = crypto.Signature{} + if noSig.Verify() == nil { + t.Errorf("transaction with no signature %#v verified incorrectly", noSig) + } + + largeWindow := stxn + largeWindow.LastValid += basics.Round(config.Protocol.MaxTxnLife) + if largeWindow.Verify() == nil { + t.Errorf("transaction with large window %#v verified incorrectly", largeWindow) + } + + badWindow := txn + badWindow.Payment.LastValid = badWindow.Payment.FirstValid - 1 + if badWindow.Verify() == nil { + t.Errorf("transaction with bad window %#v verified incorrectly", badWindow) + } + + badFee := txn + badFee.Payment.Fee = basics.MicroAlgos{} + if badFee.Verify() == nil { + t.Errorf("transaction with small fee %#v verified incorrectly", badFee) + } + + overflow := txn + overflow.Payment.Amount = basics.MicroAlgos{} + overflow.Payment.Fee = basics.MicroAlgos{Raw: 10} + if overflow.Verify() == nil { + t.Errorf("transaction with overflowing amount %#v verified incorrectly", overflow) + } + + if i > 5 { + break + } + } +} +*/ diff --git a/ledger/eval.go b/ledger/eval.go index ae637fd661..9ce3ad0f4a 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -29,6 +29,7 @@ import ( "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/verify" + "github.com/algorand/go-algorand/ledger/apply" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/util/execpool" @@ -78,7 +79,7 @@ func (x *roundCowBase) txnCounter() uint64 { return x.txnCount } -// wrappers for roundCowState to satisfy the (current) transactions.Balances interface +// wrappers for roundCowState to satisfy the (current) apply.Balances interface func (cs *roundCowState) Get(addr basics.Address, withPendingRewards bool) (basics.BalanceRecord, error) { acctdata, err := cs.lookup(addr) if err != nil { @@ -478,7 +479,7 @@ func (eval *BlockEvaluator) prepareAppEvaluators(txgroup []transactions.SignedTx } // Construct an appTealEvaluator (implements - // transactions.StateEvaluator) for use in ApplicationCall transactions. + // apply.StateEvaluator) for use in ApplicationCall transactions. steva := appTealEvaluator{ evalParams: logic.EvalParams{ Txn: &groupNoAD[i], @@ -614,7 +615,7 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, appEval *app } // Apply the transaction, updating the cow balances - applyData, err := txn.Txn.Apply(cow, appEval, spec, cow.txnCounter()) + applyData, err := applyTransaction(txn.Txn, cow, appEval, spec, cow.txnCounter()) if err != nil { return fmt.Errorf("transaction %v: %v", txid, err) } @@ -684,6 +685,71 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, appEval *app return nil } +// applyTransaction changes the balances according to this transaction. +func applyTransaction(tx transactions.Transaction, balances apply.Balances, steva apply.StateEvaluator, spec transactions.SpecialAddresses, ctr uint64) (ad transactions.ApplyData, err error) { + params := balances.ConsensusParams() + + // move fee to pool + err = balances.Move(tx.Sender, spec.FeeSink, tx.Fee, &ad.SenderRewards, nil) + if err != nil { + return + } + + // rekeying: update balrecord.AuthAddr to tx.RekeyTo if provided + if (tx.RekeyTo != basics.Address{}) { + var record basics.BalanceRecord + record, err = balances.Get(tx.Sender, false) + if err != nil { + return + } + // Special case: rekeying to the account's actual address just sets balrecord.AuthAddr to 0 + // This saves 32 bytes in your balance record if you want to go back to using your original key + if tx.RekeyTo == tx.Sender { + record.AuthAddr = basics.Address{} + } else { + record.AuthAddr = tx.RekeyTo + } + + err = balances.Put(record) + if err != nil { + return + } + } + + switch tx.Type { + case protocol.PaymentTx: + err = apply.Payment(tx.PaymentTxnFields, tx.Header, balances, spec, &ad) + + case protocol.KeyRegistrationTx: + err = apply.Keyreg(tx.KeyregTxnFields, tx.Header, balances, spec, &ad) + + case protocol.AssetConfigTx: + err = apply.AssetConfig(tx.AssetConfigTxnFields, tx.Header, balances, spec, &ad, ctr) + + case protocol.AssetTransferTx: + err = apply.AssetTransfer(tx.AssetTransferTxnFields, tx.Header, balances, spec, &ad) + + case protocol.AssetFreezeTx: + err = apply.AssetFreeze(tx.AssetFreezeTxnFields, tx.Header, balances, spec, &ad) + + case protocol.ApplicationCallTx: + err = apply.ApplicationCall(tx.ApplicationCallTxnFields, tx.Header, balances, &ad, ctr, steva) + + default: + err = fmt.Errorf("Unknown transaction type %v", tx.Type) + } + + // If the protocol does not support rewards in ApplyData, + // clear them out. + if !params.RewardsInApplyData { + ad.SenderRewards = basics.MicroAlgos{} + ad.ReceiverRewards = basics.MicroAlgos{} + ad.CloseRewards = basics.MicroAlgos{} + } + + return +} + // Call "endOfBlock" after all the block's rewards and transactions are processed. func (eval *BlockEvaluator) endOfBlock() error { if eval.generate { From 5729031d729d3ef7a1ed5e9c2c8bb4bba888b3f5 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 27 Jul 2020 16:01:35 -0400 Subject: [PATCH 176/267] make fmt --- ledger/apply/apply.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go index b72986409f..d15439e4e4 100644 --- a/ledger/apply/apply.go +++ b/ledger/apply/apply.go @@ -1,3 +1,19 @@ +// 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 . + package apply import ( From 79525f7befec8a3a88c6f1bc8efdb1aae7464676 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 16:35:02 -0400 Subject: [PATCH 177/267] Finalize TestFullCatchpointWriter --- ledger/accountdb_test.go | 82 +++++++++++++++++++++++++-------- ledger/acctupdates_test.go | 2 +- ledger/catchpointwriter_test.go | 49 ++++++++++++-------- 3 files changed, 93 insertions(+), 40 deletions(-) diff --git a/ledger/accountdb_test.go b/ledger/accountdb_test.go index c92f738ab8..5794f71a4c 100644 --- a/ledger/accountdb_test.go +++ b/ledger/accountdb_test.go @@ -56,7 +56,7 @@ func randomAccountData(rewardsLevel uint64) basics.AccountData { return data } -func randomFullAccountData(rewardsLevel uint64) basics.AccountData { +func randomFullAccountData(rewardsLevel, lastCreatableID uint64) (basics.AccountData, uint64) { data := randomAccountData(rewardsLevel) crypto.RandBytes(data.VoteID[:]) @@ -82,11 +82,12 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { crypto.RandBytes(ap.Reserve[:]) crypto.RandBytes(ap.Freeze[:]) crypto.RandBytes(ap.Clawback[:]) - data.AssetParams[basics.AssetIndex(crypto.RandUint64()%50000)] = ap + lastCreatableID++ + data.AssetParams[basics.AssetIndex(lastCreatableID)] = ap } } if 1 == (crypto.RandUint64() % 2) { - // if account owns assets/applications + // if account owns assets data.Assets = make(map[basics.AssetIndex]basics.AssetHolding) ownedAssetsCount := crypto.RandUint64()%20 + 1 for i := uint64(0); i < ownedAssetsCount; i++ { @@ -94,7 +95,7 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { Amount: crypto.RandUint64(), Frozen: (crypto.RandUint64()%2 == 0), } - data.Assets[basics.AssetIndex(crypto.RandUint64()%50000)] = ah + data.Assets[basics.AssetIndex(crypto.RandUint64()%lastCreatableID)] = ah } } if 1 == (crypto.RandUint64() % 5) { @@ -112,14 +113,16 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { }, KeyValue: make(map[string]basics.TealValue), } - appName := fmt.Sprintf("lapp%x", crypto.RandUint64()) + for i := uint64(0); i < ap.Schema.NumUint; i++ { + appName := fmt.Sprintf("lapp%x-%x", crypto.RandUint64(), i) ap.KeyValue[appName] = basics.TealValue{ Type: basics.TealUintType, Uint: crypto.RandUint64(), } } for i := uint64(0); i < ap.Schema.NumByteSlice; i++ { + appName := fmt.Sprintf("lapp%x-%x", crypto.RandUint64(), i) tv := basics.TealValue{ Type: basics.TealBytesType, } @@ -128,7 +131,10 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { tv.Bytes = string(bytes) ap.KeyValue[appName] = tv } - data.AppLocalStates[basics.AppIndex(crypto.RandUint64()%50000)] = ap + if len(ap.KeyValue) == 0 { + ap.KeyValue = nil + } + data.AppLocalStates[basics.AppIndex(crypto.RandUint64()%lastCreatableID)] = ap } } @@ -140,25 +146,60 @@ func randomFullAccountData(rewardsLevel uint64) basics.AccountData { } if 1 == (crypto.RandUint64() % 3) { data.AppParams = make(map[basics.AppIndex]basics.AppParams) - appParamsCount := crypto.RandUint64()%20 + 1 + appParamsCount := crypto.RandUint64()%5 + 1 for i := uint64(0); i < appParamsCount; i++ { ap := basics.AppParams{ - ApprovalProgram: make([]byte, int(crypto.RandUint64())%config.MaxAppProgramLen), - ClearStateProgram: make([]byte, int(crypto.RandUint64())%config.MaxAppProgramLen), + ApprovalProgram: make([]byte, int(crypto.RandUint63())%config.MaxAppProgramLen), + ClearStateProgram: make([]byte, int(crypto.RandUint63())%config.MaxAppProgramLen), + GlobalState: make(basics.TealKeyValue), + StateSchemas: basics.StateSchemas{ + LocalStateSchema: basics.StateSchema{ + NumUint: crypto.RandUint64() % 5, + NumByteSlice: crypto.RandUint64() % 5, + }, + GlobalStateSchema: basics.StateSchema{ + NumUint: crypto.RandUint64() % 5, + NumByteSlice: crypto.RandUint64() % 5, + }, + }, + } + if len(ap.ApprovalProgram) > 0 { + crypto.RandBytes(ap.ApprovalProgram[:]) + } else { + ap.ApprovalProgram = nil } - crypto.RandBytes(ap.ApprovalProgram[:]) - crypto.RandBytes(ap.ClearStateProgram[:]) - data.AppParams[basics.AppIndex(crypto.RandUint64()%50000)] = ap + if len(ap.ClearStateProgram) > 0 { + crypto.RandBytes(ap.ClearStateProgram[:]) + } else { + ap.ClearStateProgram = nil + } + + for i := uint64(0); i < ap.StateSchemas.LocalStateSchema.NumUint+ap.StateSchemas.GlobalStateSchema.NumUint; i++ { + appName := fmt.Sprintf("tapp%x-%x", crypto.RandUint64(), i) + ap.GlobalState[appName] = basics.TealValue{ + Type: basics.TealUintType, + Uint: crypto.RandUint64(), + } + } + for i := uint64(0); i < ap.StateSchemas.LocalStateSchema.NumByteSlice+ap.StateSchemas.GlobalStateSchema.NumByteSlice; i++ { + appName := fmt.Sprintf("tapp%x-%x", crypto.RandUint64(), i) + tv := basics.TealValue{ + Type: basics.TealBytesType, + } + bytes := make([]byte, crypto.RandUint64()%512) + crypto.RandBytes(bytes[:]) + tv.Bytes = string(bytes) + ap.GlobalState[appName] = tv + } + if len(ap.GlobalState) == 0 { + ap.GlobalState = nil + } + lastCreatableID++ + data.AppParams[basics.AppIndex(lastCreatableID)] = ap } } - //fmt.Printf("%v\n", data.SelectionID) - /* - // AppParams stores the global parameters and state associated with any - // applications that this account has created. - AppParams map[AppIndex]AppParams `codec:"appp,allocbound=encodedMaxAppParams"` - */ - return data + return data, lastCreatableID } func randomAccounts(niter int, simpleAccounts bool) map[basics.Address]basics.AccountData { @@ -168,8 +209,9 @@ func randomAccounts(niter int, simpleAccounts bool) map[basics.Address]basics.Ac res[randomAddress()] = randomAccountData(0) } } else { + lastCreatableID := crypto.RandUint64() % 512 for i := 0; i < niter; i++ { - res[randomAddress()] = randomFullAccountData(0) + res[randomAddress()], lastCreatableID = randomFullAccountData(0, lastCreatableID) } } return res diff --git a/ledger/acctupdates_test.go b/ledger/acctupdates_test.go index 4a33ad863a..28931c234c 100644 --- a/ledger/acctupdates_test.go +++ b/ledger/acctupdates_test.go @@ -542,7 +542,7 @@ func TestLargeAccountCountCatchpointGeneration(t *testing.T) { ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(100000, false)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(100000, true)} rewardsLevels := []uint64{0} pooldata := basics.AccountData{} diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index 466ef3d55c..368e41b818 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "context" + "database/sql" "fmt" "io" "io/ioutil" @@ -195,6 +196,7 @@ func TestBasicCatchpointWriter(t *testing.T) { break } } + if header.Name == "content.msgpack" { var fileHeader CatchpointFileHeader err = protocol.Decode(balancesBlockBytes, &fileHeader) @@ -214,9 +216,9 @@ func TestBasicCatchpointWriter(t *testing.T) { } } -func TestCatchpointWriterWithApplicationsData(t *testing.T) { +func TestFullCatchpointWriter(t *testing.T) { // create new protocol version, which has lower back balance. - testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestCatchpointWriterWithApplicationsData") + testProtocolVersion := protocol.ConsensusVersion("test-protocol-TestFullCatchpointWriter") protoParams := config.Consensus[protocol.ConsensusCurrentVersion] protoParams.MaxBalLookback = 32 protoParams.SeedLookback = 2 @@ -231,7 +233,7 @@ func TestCatchpointWriterWithApplicationsData(t *testing.T) { ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(300, false)} + accts := []map[basics.Address]basics.AccountData{randomAccounts(BalancesPerCatchpointFileChunk*3, false)} au := &accountUpdates{} conf := config.GetDefaultLocal() @@ -255,12 +257,22 @@ func TestCatchpointWriterWithApplicationsData(t *testing.T) { } } + // create a ledger. + l, err := OpenLedger(ml.log, "TestFullCatchpointWriter", true, InitState{}, conf) + require.NoError(t, err) + defer l.Close() + accessor := MakeCatchpointCatchupAccessor(l, l.log) + + err = accessor.ResetStagingBalances(context.Background(), true) + require.NoError(t, err) + // load the file from disk. fileContent, err := ioutil.ReadFile(fileName) require.NoError(t, err) gzipReader, err := gzip.NewReader(bytes.NewBuffer(fileContent)) require.NoError(t, err) tarReader := tar.NewReader(gzipReader) + var catchupProgress CatchpointCatchupAccessorProgress defer gzipReader.Close() for { header, err := tarReader.Next() @@ -287,21 +299,20 @@ func TestCatchpointWriterWithApplicationsData(t *testing.T) { break } } - if header.Name == "content.msgpack" { - var fileHeader CatchpointFileHeader - err = protocol.Decode(balancesBlockBytes, &fileHeader) - require.NoError(t, err) - require.Equal(t, catchpointLabel, fileHeader.Catchpoint) - require.Equal(t, blocksRound, fileHeader.BlocksRound) - require.Equal(t, blockHeaderDigest, fileHeader.BlockHeaderDigest) - require.Equal(t, uint64(len(accts[0])), fileHeader.TotalAccounts) - } else if header.Name == "balances.1.1.msgpack" { - var balances catchpointFileBalancesChunk - err = protocol.Decode(balancesBlockBytes, &balances) - require.NoError(t, err) - require.Equal(t, uint64(len(accts[0])), uint64(len(balances.Balances))) - } else { - require.Failf(t, "unexpected tar chunk name %s", header.Name) - } + err = accessor.ProgressStagingBalances(context.Background(), header.Name, balancesBlockBytes, &catchupProgress) + require.NoError(t, err) + } + + err = l.trackerDBs.wdb.Atomic(func(ctx context.Context, tx *sql.Tx) error { + err := applyCatchpointStagingBalances(ctx, tx, 0) + return err + }) + require.NoError(t, err) + + // verify that the account data aligns with what we originally stored : + for addr, acct := range accts[0] { + acctData, err := l.LookupWithoutRewards(0, addr) + require.NoError(t, err) + require.Equal(t, acct, acctData) } } From fd34e2b609fdc29b26ca16c9f426eb26b0570155 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 17:19:02 -0400 Subject: [PATCH 178/267] Add waits to TestArchivalCreatables --- ledger/archival_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 551b638dd7..8c7a49d0fb 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -410,8 +410,10 @@ func TestArchivalCreatables(t *testing.T) { // Add the block err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) + + l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - l.WaitForCommit(blk.Round()) + l.Wait(blk.Round()) // check that we can fetch creator for all created assets/apps and // can't for deleted assets/apps @@ -591,8 +593,9 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) + l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - l.WaitForCommit(blk.Round()) + l.Wait(blk.Round()) // close and reopen the same DB l.Close() From 65db0ab633b519fbc290c909279ff5b7e9133bf1 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 18:12:34 -0400 Subject: [PATCH 179/267] update. --- ledger/archival_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 8c7a49d0fb..fbae7d7a97 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -185,6 +185,7 @@ func TestArchivalRestart(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) + <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } l.WaitForCommit(blk.Round()) @@ -411,9 +412,9 @@ func TestArchivalCreatables(t *testing.T) { err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) + <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - l.Wait(blk.Round()) + <-l.Wait(blk.Round()) // check that we can fetch creator for all created assets/apps and // can't for deleted assets/apps @@ -593,9 +594,9 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) + <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - l.Wait(blk.Round()) + <-l.Wait(blk.Round()) // close and reopen the same DB l.Close() @@ -677,6 +678,7 @@ func TestArchivalFromNonArchival(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) + <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } l.WaitForCommit(blk.Round()) From 5653bf57409ca449f7bbb9d0d23d3ccb4821c1a6 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Mon, 27 Jul 2020 19:53:47 -0400 Subject: [PATCH 180/267] -- Modify KMD to support signatures with a different address than the sender. --- cmd/goal/multisig.go | 6 +++++- daemon/kmd/api/v1/handlers.go | 2 +- daemon/kmd/client/wrappers.go | 3 ++- daemon/kmd/lib/kmdapi/requests.go | 1 + daemon/kmd/wallet/driver/ledger.go | 2 +- daemon/kmd/wallet/driver/sqlite.go | 8 +++++++- daemon/kmd/wallet/driver/sqlite_errors.go | 2 +- daemon/kmd/wallet/wallet.go | 2 +- libgoal/transactions.go | 25 ++++++++++++++++++++++- 9 files changed, 43 insertions(+), 8 deletions(-) diff --git a/cmd/goal/multisig.go b/cmd/goal/multisig.go index 3ec5a26d31..999fd02286 100644 --- a/cmd/goal/multisig.go +++ b/cmd/goal/multisig.go @@ -117,7 +117,11 @@ var addSigCmd = &cobra.Command{ reportErrorf(msigParseError, err) } } else { - msig, err = client.MultisigSignTransactionWithWallet(wh, pw, stxn.Txn, addr, stxn.Msig) + if stxn.AuthAddr.IsZero() { + msig, err = client.MultisigSignTransactionWithWallet(wh, pw, stxn.Txn, addr, stxn.Msig) + } else { + msig, err = client.MultisigSignTransactionWithWalletAndSigner(wh, pw, stxn.Txn, addr, stxn.Msig, stxn.AuthAddr.GetUserAddress()) + } if err != nil { reportErrorf(errorSigningTX, err) } diff --git a/daemon/kmd/api/v1/handlers.go b/daemon/kmd/api/v1/handlers.go index 8b8e9abe20..cf6777bee0 100644 --- a/daemon/kmd/api/v1/handlers.go +++ b/daemon/kmd/api/v1/handlers.go @@ -1127,7 +1127,7 @@ func postMultisigTransactionSignHandler(ctx reqContext, w http.ResponseWriter, r } // Sign the transaction - msig, err := wallet.MultisigSignTransaction(tx, req.PublicKey, req.PartialMsig, []byte(req.WalletPassword)) + msig, err := wallet.MultisigSignTransaction(tx, req.PublicKey, req.PartialMsig, []byte(req.WalletPassword), req.AuthAddr) if err != nil { errorResponse(w, http.StatusBadRequest, err) return diff --git a/daemon/kmd/client/wrappers.go b/daemon/kmd/client/wrappers.go index 3f4a010dd9..e407bc1533 100644 --- a/daemon/kmd/client/wrappers.go +++ b/daemon/kmd/client/wrappers.go @@ -141,13 +141,14 @@ func (kcl KMDClient) DeleteMultisigAddr(walletHandle []byte, pw []byte, addr str } // MultisigSignTransaction wraps kmdapi.APIV1POSTMultisigTransactionSignRequest -func (kcl KMDClient) MultisigSignTransaction(walletHandle, pw []byte, tx []byte, pk crypto.PublicKey, partial crypto.MultisigSig) (resp kmdapi.APIV1POSTMultisigTransactionSignResponse, err error) { +func (kcl KMDClient) MultisigSignTransaction(walletHandle, pw []byte, tx []byte, pk crypto.PublicKey, partial crypto.MultisigSig, msigSigner crypto.Digest) (resp kmdapi.APIV1POSTMultisigTransactionSignResponse, err error) { req := kmdapi.APIV1POSTMultisigTransactionSignRequest{ WalletHandleToken: string(walletHandle), WalletPassword: string(pw), Transaction: tx, PublicKey: pk, PartialMsig: partial, + AuthAddr: msigSigner, } err = kcl.DoV1Request(req, &resp) return diff --git a/daemon/kmd/lib/kmdapi/requests.go b/daemon/kmd/lib/kmdapi/requests.go index 19bd2e914b..6cda189a29 100644 --- a/daemon/kmd/lib/kmdapi/requests.go +++ b/daemon/kmd/lib/kmdapi/requests.go @@ -228,6 +228,7 @@ type APIV1POSTMultisigTransactionSignRequest struct { PublicKey crypto.PublicKey `json:"public_key"` PartialMsig crypto.MultisigSig `json:"partial_multisig"` WalletPassword string `json:"wallet_password"` + AuthAddr crypto.Digest `json:"signer"` } // APIV1POSTMultisigProgramSignRequest is the request for `POST /v1/multisig/signprogram` diff --git a/daemon/kmd/wallet/driver/ledger.go b/daemon/kmd/wallet/driver/ledger.go index a28ad83443..b2fe6fea24 100644 --- a/daemon/kmd/wallet/driver/ledger.go +++ b/daemon/kmd/wallet/driver/ledger.go @@ -355,7 +355,7 @@ func (lw *LedgerWallet) SignProgram(data []byte, src crypto.Digest, pw []byte) ( } // MultisigSignTransaction implements the Wallet interface. -func (lw *LedgerWallet) MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte) (crypto.MultisigSig, error) { +func (lw *LedgerWallet) MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte, signer crypto.Digest) (crypto.MultisigSig, error) { isValidKey := false for i := 0; i < len(partial.Subsigs); i++ { subsig := &partial.Subsigs[i] diff --git a/daemon/kmd/wallet/driver/sqlite.go b/daemon/kmd/wallet/driver/sqlite.go index 77675d7509..3620adc677 100644 --- a/daemon/kmd/wallet/driver/sqlite.go +++ b/daemon/kmd/wallet/driver/sqlite.go @@ -1151,7 +1151,7 @@ func (sw *SQLiteWallet) SignProgram(data []byte, src crypto.Digest, pw []byte) ( // MultisigSignTransaction starts a multisig signature or adds a signature to a // partially signed multisig transaction signature of the passed transaction // using the key -func (sw *SQLiteWallet) MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte) (sig crypto.MultisigSig, err error) { +func (sw *SQLiteWallet) MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte, signer crypto.Digest) (sig crypto.MultisigSig, err error) { // Check the password err = sw.CheckPassword(pw) if err != nil { @@ -1199,6 +1199,12 @@ func (sw *SQLiteWallet) MultisigSignTransaction(tx transactions.Transaction, pk return } + // Check that the multisig address equals to either sender or signer + if addr != crypto.Digest(tx.Src()) && addr != signer { + err = errMsigWrongAddr + return + } + // Check that key is one of the ones in the preimage err = errMsigWrongKey for _, subsig := range partial.Subsigs { diff --git a/daemon/kmd/wallet/driver/sqlite_errors.go b/daemon/kmd/wallet/driver/sqlite_errors.go index 003bfd3c02..01266021c3 100644 --- a/daemon/kmd/wallet/driver/sqlite_errors.go +++ b/daemon/kmd/wallet/driver/sqlite_errors.go @@ -43,5 +43,5 @@ var errWalletNotFound = fmt.Errorf("wallet not found") var errSQLiteWrongType = fmt.Errorf("sqlite wallet driver returned wrong wallet type") var errNameTooLong = fmt.Errorf("wallet name too long, must be <= %d bytes", sqliteMaxWalletNameLen) var errIDTooLong = fmt.Errorf("wallet id too long, must be <= %d bytes", sqliteMaxWalletIDLen) -var errMsigWrongAddr = fmt.Errorf("given multisig preimage hashes to wrong address") +var errMsigWrongAddr = fmt.Errorf("given multisig preimage hashes to neither Sender or AuthAddr") var errMsigWrongKey = fmt.Errorf("given key is not a possible signer for this multisig") diff --git a/daemon/kmd/wallet/wallet.go b/daemon/kmd/wallet/wallet.go index d4fc817a95..a409352685 100644 --- a/daemon/kmd/wallet/wallet.go +++ b/daemon/kmd/wallet/wallet.go @@ -53,7 +53,7 @@ type Wallet interface { SignTransaction(tx transactions.Transaction, pk crypto.PublicKey, pw []byte) ([]byte, error) - MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte) (crypto.MultisigSig, error) + MultisigSignTransaction(tx transactions.Transaction, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte, signer crypto.Digest) (crypto.MultisigSig, error) SignProgram(program []byte, src crypto.Digest, pw []byte) ([]byte, error) MultisigSignProgram(program []byte, src crypto.Digest, pk crypto.PublicKey, partial crypto.MultisigSig, pw []byte) (crypto.MultisigSig, error) diff --git a/libgoal/transactions.go b/libgoal/transactions.go index 514ce5d5e8..40609bd7b7 100644 --- a/libgoal/transactions.go +++ b/libgoal/transactions.go @@ -104,7 +104,30 @@ func (c *Client) MultisigSignTransactionWithWallet(walletHandle, pw []byte, utx if err != nil { return } - resp, err := kmd.MultisigSignTransaction(walletHandle, pw, txBytes, crypto.PublicKey(addr), partial) + resp, err := kmd.MultisigSignTransaction(walletHandle, pw, txBytes, crypto.PublicKey(addr), partial, crypto.Digest{}) + if err != nil { + return + } + err = protocol.Decode(resp.Multisig, &msig) + return +} + +// MultisigSignTransactionWithWallet creates a multisig (or adds to an existing partial multisig, if one is provided), signing with the key corresponding to the given address and using the specified wallet +func (c *Client) MultisigSignTransactionWithWalletAndSigner(walletHandle, pw []byte, utx transactions.Transaction, signerAddr string, partial crypto.MultisigSig, signerMsig string) (msig crypto.MultisigSig, err error) { + txBytes := protocol.Encode(&utx) + addr, err := basics.UnmarshalChecksumAddress(signerAddr) + if err != nil { + return + } + msigAddr, err := basics.UnmarshalChecksumAddress(signerMsig) + if err != nil { + return + } + kmd, err := c.ensureKmdClient() + if err != nil { + return + } + resp, err := kmd.MultisigSignTransaction(walletHandle, pw, txBytes, crypto.PublicKey(addr), partial, crypto.Digest(msigAddr)) if err != nil { return } From eeb1e78738b18dcde2b493cbf282a7727d44fba4 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Mon, 27 Jul 2020 20:15:21 -0400 Subject: [PATCH 181/267] -- Making go lint happy --- cmd/goal/clerk.go | 2 +- cmd/goal/messages.go | 2 +- libgoal/transactions.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 3aabe1c7e4..5785804b51 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -282,7 +282,7 @@ var sendCmd = &cobra.Command{ // --msig-params is invalid without -o if outFilename == "" && msigParams != "" { - reportErrorln(NoOutputFileError) + reportErrorln(noOutputFileError) } checkTxValidityPeriodCmdFlags(cmd) diff --git a/cmd/goal/messages.go b/cmd/goal/messages.go index 95d6ef1d20..868447aaf6 100644 --- a/cmd/goal/messages.go +++ b/cmd/goal/messages.go @@ -120,7 +120,7 @@ const ( msigParseError = "Multisig information parsing error: %s" failDecodeAddressError = "Cannot decode address: %v" rekeySenderTargetSameError = "The sender and the resulted multisig address are the same" - NoOutputFileError = "--msig-params must be specified with an output file name (-o)" + noOutputFileError = "--msig-params must be specified with an output file name (-o)" infoAutoFeeSet = "Automatically set fee to %d MicroAlgos" loggingNotConfigured = "Remote logging is not currently configured and won't be enabled" diff --git a/libgoal/transactions.go b/libgoal/transactions.go index 40609bd7b7..5cb68c57bc 100644 --- a/libgoal/transactions.go +++ b/libgoal/transactions.go @@ -112,7 +112,7 @@ func (c *Client) MultisigSignTransactionWithWallet(walletHandle, pw []byte, utx return } -// MultisigSignTransactionWithWallet creates a multisig (or adds to an existing partial multisig, if one is provided), signing with the key corresponding to the given address and using the specified wallet +// MultisigSignTransactionWithWalletAndSigner creates a multisig (or adds to an existing partial multisig, if one is provided), signing with the key corresponding to the given address and using the specified wallet func (c *Client) MultisigSignTransactionWithWalletAndSigner(walletHandle, pw []byte, utx transactions.Transaction, signerAddr string, partial crypto.MultisigSig, signerMsig string) (msig crypto.MultisigSig, err error) { txBytes := protocol.Encode(&utx) addr, err := basics.UnmarshalChecksumAddress(signerAddr) From c0bddbbc43946b82bf090435fc7ff9158cad6ee3 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 27 Jul 2020 20:35:43 -0400 Subject: [PATCH 182/267] Add error code testing for unit test --- ledger/ledger_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ledger/ledger_test.go b/ledger/ledger_test.go index 7ca813bdc5..bf035855e4 100644 --- a/ledger/ledger_test.go +++ b/ledger/ledger_test.go @@ -903,10 +903,13 @@ func TestLedgerReload(t *testing.T) { for i := 0; i < 128; i++ { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) - l.AddBlock(blk, agreement.Certificate{}) + err = l.AddBlock(blk, agreement.Certificate{}) + require.NoError(t, err) if i%7 == 0 { - l.reloadLedger() + err = l.reloadLedger() + require.NoError(t, err) + // if we reloaded it before it got committed, we need to roll back the round counter. if l.LatestCommitted() != blk.BlockHeader.Round { blk.BlockHeader.Round = l.LatestCommitted() From 3481e69a52d7f94dd284985ffe26cde44dc83b5b Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Tue, 28 Jul 2020 00:32:39 -0400 Subject: [PATCH 183/267] -- fix a typo --- daemon/kmd/wallet/driver/sqlite_errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/kmd/wallet/driver/sqlite_errors.go b/daemon/kmd/wallet/driver/sqlite_errors.go index 01266021c3..7e64138aa5 100644 --- a/daemon/kmd/wallet/driver/sqlite_errors.go +++ b/daemon/kmd/wallet/driver/sqlite_errors.go @@ -43,5 +43,5 @@ var errWalletNotFound = fmt.Errorf("wallet not found") var errSQLiteWrongType = fmt.Errorf("sqlite wallet driver returned wrong wallet type") var errNameTooLong = fmt.Errorf("wallet name too long, must be <= %d bytes", sqliteMaxWalletNameLen) var errIDTooLong = fmt.Errorf("wallet id too long, must be <= %d bytes", sqliteMaxWalletIDLen) -var errMsigWrongAddr = fmt.Errorf("given multisig preimage hashes to neither Sender or AuthAddr") +var errMsigWrongAddr = fmt.Errorf("given multisig preimage hashes to neither Sender nor AuthAddr") var errMsigWrongKey = fmt.Errorf("given key is not a possible signer for this multisig") From f01ef79b37b95ead47a028c38f216c46e0673f72 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 08:53:46 -0400 Subject: [PATCH 184/267] Avoid creating arrays of account maps needlessly. --- ledger/catchpointwriter_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ledger/catchpointwriter_test.go b/ledger/catchpointwriter_test.go index 368e41b818..c64b01af8f 100644 --- a/ledger/catchpointwriter_test.go +++ b/ledger/catchpointwriter_test.go @@ -140,13 +140,13 @@ func TestBasicCatchpointWriter(t *testing.T) { ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(300, false)} + accts := randomAccounts(300, false) au := &accountUpdates{} conf := config.GetDefaultLocal() conf.CatchpointInterval = 1 conf.Archival = true - au.initialize(conf, ".", protoParams, accts[0]) + au.initialize(conf, ".", protoParams, accts) defer au.close() err := au.loadFromDisk(ml) require.NoError(t, err) @@ -204,12 +204,12 @@ func TestBasicCatchpointWriter(t *testing.T) { require.Equal(t, catchpointLabel, fileHeader.Catchpoint) require.Equal(t, blocksRound, fileHeader.BlocksRound) require.Equal(t, blockHeaderDigest, fileHeader.BlockHeaderDigest) - require.Equal(t, uint64(len(accts[0])), fileHeader.TotalAccounts) + require.Equal(t, uint64(len(accts)), fileHeader.TotalAccounts) } else if header.Name == "balances.1.1.msgpack" { var balances catchpointFileBalancesChunk err = protocol.Decode(balancesBlockBytes, &balances) require.NoError(t, err) - require.Equal(t, uint64(len(accts[0])), uint64(len(balances.Balances))) + require.Equal(t, uint64(len(accts)), uint64(len(balances.Balances))) } else { require.Failf(t, "unexpected tar chunk name %s", header.Name) } @@ -233,13 +233,13 @@ func TestFullCatchpointWriter(t *testing.T) { ml := makeMockLedgerForTracker(t, true) defer ml.close() ml.blocks = randomInitChain(testProtocolVersion, 10) - accts := []map[basics.Address]basics.AccountData{randomAccounts(BalancesPerCatchpointFileChunk*3, false)} + accts := randomAccounts(BalancesPerCatchpointFileChunk*3, false) au := &accountUpdates{} conf := config.GetDefaultLocal() conf.CatchpointInterval = 1 conf.Archival = true - au.initialize(conf, ".", protoParams, accts[0]) + au.initialize(conf, ".", protoParams, accts) defer au.close() err := au.loadFromDisk(ml) require.NoError(t, err) @@ -310,7 +310,7 @@ func TestFullCatchpointWriter(t *testing.T) { require.NoError(t, err) // verify that the account data aligns with what we originally stored : - for addr, acct := range accts[0] { + for addr, acct := range accts { acctData, err := l.LookupWithoutRewards(0, addr) require.NoError(t, err) require.Equal(t, acct, acctData) From 27a1ca14b87545b032367d22e8b3907bb13c4a0f Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Tue, 28 Jul 2020 10:28:50 -0400 Subject: [PATCH 185/267] update comment --- ledger/apply/apply.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go index d15439e4e4..1791182846 100644 --- a/ledger/apply/apply.go +++ b/ledger/apply/apply.go @@ -48,8 +48,10 @@ type Balances interface { } // StateEvaluator is an interface that provides some Stateful TEAL -// functionality that may be passed through to Apply from ledger, avoiding a -// circular dependency between the logic and transactions packages +// functionality that may be passed through to Apply from ledger. It was +// originally created to avoid a circular dependency between the logic and +// transactions packages (when the apply methods were in the transactions +// package). type StateEvaluator interface { Eval(program []byte) (pass bool, stateDelta basics.EvalDelta, err error) Check(program []byte) (cost int, err error) From ca71087a3286d9f6aa1bf4b29708d0beb4e86f98 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 10:56:09 -0400 Subject: [PATCH 186/267] Disable the deadlock on disk-storage unit tests. --- ledger/archival_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index fbae7d7a97..03b836b303 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -31,6 +31,8 @@ import ( "github.com/stretchr/testify/require" + "github.com/algorand/go-deadlock" + "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" @@ -165,6 +167,9 @@ func TestArchival(t *testing.T) { func TestArchivalRestart(t *testing.T) { // Start in archival mode, add 2K blocks, restart, ensure all blocks are there + // disable deadlock checking code + deadlock.Opts.Disable = true + dbTempDir, err := ioutil.TempDir("", "testdir"+t.Name()) require.NoError(t, err) dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) @@ -656,7 +661,7 @@ func makeSignedTxnInBlock(tx transactions.Transaction) transactions.SignedTxnInB func TestArchivalFromNonArchival(t *testing.T) { // Start in non-archival mode, add 2K blocks, restart in archival mode ensure only genesis block is there - + deadlock.Opts.Disable = true dbTempDir, err := ioutil.TempDir(os.TempDir(), "testdir") require.NoError(t, err) dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) From 4d12fa24623851b0ecdb1f0125d84216446b701d Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 11:50:16 -0400 Subject: [PATCH 187/267] Add closed to blockNotifier --- ledger/notifier.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ledger/notifier.go b/ledger/notifier.go index 61a76ef6ec..bb06b62b93 100644 --- a/ledger/notifier.go +++ b/ledger/notifier.go @@ -41,9 +41,12 @@ type blockNotifier struct { listeners []BlockListener pendingBlocks []blockDeltaPair running bool + // closed is used to syncronize closing the worker goroutine. It's being created during loadFromDisk, and the worker is responsible to close it once it's aborting it's goroutine. The close function waits on this to complete. + closed chan struct{} } -func (bn *blockNotifier) worker() { +func (bn *blockNotifier) worker(closed chan struct{}) { + defer close(closed) bn.mu.Lock() for { @@ -73,19 +76,20 @@ func (bn *blockNotifier) worker() { func (bn *blockNotifier) close() { bn.mu.Lock() - defer bn.mu.Unlock() if bn.running { bn.running = false bn.cond.Broadcast() } + bn.mu.Unlock() + <-bn.closed } func (bn *blockNotifier) loadFromDisk(l ledgerForTracker) error { bn.cond = sync.NewCond(&bn.mu) bn.running = true bn.pendingBlocks = nil - - go bn.worker() + bn.closed = make(chan struct{}) + go bn.worker(bn.closed) return nil } From 79f7fb0081f260e0c25d1e73d6d4f83d46d249b9 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Tue, 28 Jul 2020 12:44:46 -0400 Subject: [PATCH 188/267] Add eval delta to PendingTransactionByID --- daemon/algod/api/algod.oas2.json | 11 + daemon/algod/api/algod.oas3.yml | 20 ++ .../api/server/v2/generated/private/routes.go | 249 +++++++++--------- .../api/server/v2/generated/private/types.go | 6 + .../algod/api/server/v2/generated/routes.go | 232 ++++++++-------- daemon/algod/api/server/v2/generated/types.go | 6 + daemon/algod/api/server/v2/handlers.go | 20 +- daemon/algod/api/server/v2/utils.go | 50 ++++ 8 files changed, 347 insertions(+), 247 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 15edc838bf..05d513e22e 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -2097,6 +2097,17 @@ "description": "Rewards in microalgos applied to the sender account.", "type": "integer" }, + "local-state-delta": { + "description": "\\[ld\\] Local state key/value changes for the application being executed by this transaction.", + "type": "array", + "items": { + "$ref": "#/definitions/AccountStateDelta" + } + }, + "global-state-delta": { + "description": "\\[gd\\] Global state key/value changes for the application being executed by this transaction.", + "$ref": "#/definitions/StateDelta" + }, "txn": { "description": "The raw signed transaction.", "type": "object", diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index c375753a11..3f2f700a54 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -475,6 +475,16 @@ "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" @@ -3016,6 +3026,16 @@ "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" diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 445b005dd8..20b205d497 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -235,130 +235,131 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "TGHjh4aoHz0LeOLefLj5vwEAAP//HPdY54CEAAA=", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index 9d62f9e774..bde0d54ebd 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -537,6 +537,12 @@ type PendingTransactionResponse struct { // The round where this transaction was confirmed, if present. ConfirmedRound *uint64 `json:"confirmed-round,omitempty"` + // Application state delta. + GlobalStateDelta *StateDelta `json:"global-state-delta,omitempty"` + + // \[ld\] Local state key/value changes for the application being executed by this transaction. + LocalStateDelta *[]AccountStateDelta `json:"local-state-delta,omitempty"` + // 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. PoolError string `json:"pool-error"` diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 5e8f24a4d1..2c061ccbd5 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -600,121 +600,123 @@ var swaggerSpec = []string{ "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", - "HwAA//8lx6pdWdcAAA==", + "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", + "yBp12QAA", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index 1e849902f1..db5017e151 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -537,6 +537,12 @@ type PendingTransactionResponse struct { // The round where this transaction was confirmed, if present. ConfirmedRound *uint64 `json:"confirmed-round,omitempty"` + // Application state delta. + GlobalStateDelta *StateDelta `json:"global-state-delta,omitempty"` + + // \[ld\] Local state key/value changes for the application being executed by this transaction. + LocalStateDelta *[]AccountStateDelta `json:"local-state-delta,omitempty"` + // 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. PoolError string `json:"pool-error"` diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index 09a6bb5d06..e21774e7c4 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -442,14 +442,16 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, // Encoding wasn't working well without embedding "real" objects. response := struct { - AssetIndex *uint64 `codec:"asset-index,omitempty"` - CloseRewards *uint64 `codec:"close-rewards,omitempty"` - ClosingAmount *uint64 `codec:"closing-amount,omitempty"` - ConfirmedRound *uint64 `codec:"confirmed-round,omitempty"` - PoolError string `codec:"pool-error"` - ReceiverRewards *uint64 `codec:"receiver-rewards,omitempty"` - SenderRewards *uint64 `codec:"sender-rewards,omitempty"` - Txn transactions.SignedTxn `codec:"txn"` + AssetIndex *uint64 `codec:"asset-index,omitempty"` + CloseRewards *uint64 `codec:"close-rewards,omitempty"` + ClosingAmount *uint64 `codec:"closing-amount,omitempty"` + ConfirmedRound *uint64 `codec:"confirmed-round,omitempty"` + GlobalStateDelta *generated.StateDelta `codec:"global-state-delta,omitempty"` + LocalStateDelta *[]generated.AccountStateDelta `codec:"local-state-delta,omitempty"` + PoolError string `codec:"pool-error"` + ReceiverRewards *uint64 `codec:"receiver-rewards,omitempty"` + SenderRewards *uint64 `codec:"sender-rewards,omitempty"` + Txn transactions.SignedTxn `codec:"txn"` }{ Txn: txn.Txn, } @@ -469,6 +471,8 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response.CloseRewards = &txn.ApplyData.CloseRewards.Raw response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.Ledger()) + + response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn) } data, err := encode(handle, response) diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index 399efbd44e..f87b333a83 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -17,6 +17,7 @@ package v2 import ( + "encoding/base64" "fmt" "net/http" "strings" @@ -178,3 +179,52 @@ func decode(handle codec.Handle, data []byte, v interface{}) error { } return nil } + +func convertToDeltas(txn node.TxnWithStatus) (*[]generated.AccountStateDelta, *generated.StateDelta) { + // Helper to convert basics.StateDelta -> *generated.StateDelta + convertDelta := func (d basics.StateDelta) *generated.StateDelta { + if len(d) == 0 { + return nil + } + var delta generated.StateDelta + for k, v:= range d { + delta = append(delta, generated.EvalDeltaKeyValue{ + Key: base64.StdEncoding.EncodeToString([]byte(k)), + Value: generated.EvalDelta{ + Action: uint64(v.Action), + Bytes: strOrNil(base64.StdEncoding.EncodeToString([]byte(v.Bytes))), + Uint: numOrNil(v.Uint), + }, + }) + } + return &delta + } + + var localStateDelta *[]generated.AccountStateDelta + if len(txn.ApplyData.EvalDelta.LocalDeltas) > 0 { + d := make([]generated.AccountStateDelta, 0) + accounts := txn.Txn.Txn.Accounts + + for k, v := range txn.ApplyData.EvalDelta.LocalDeltas { + // Resolve address from index + var addr string + if k == 0 { + addr = txn.Txn.Txn.Sender.String() + } else { + if int(k - 1) < len(accounts) { + addr = txn.Txn.Txn.Accounts[k-1].String() + } else { + addr = fmt.Sprintf("Invalid Address Index: %d", k - 1) + } + } + d = append(d, generated.AccountStateDelta{ + Address: addr, + Delta: *(convertDelta(v)), + }) + } + + localStateDelta = &d + } + + return localStateDelta, convertDelta(txn.ApplyData.EvalDelta.GlobalDelta) +} From 6fda502a29dfc57cc7f00237025333bc7df69555 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 14:10:58 -0400 Subject: [PATCH 189/267] update. --- ledger/archival_test.go | 8 ++------ ledger/notifier.go | 5 ++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 03b836b303..20ed8135ca 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -190,7 +190,6 @@ func TestArchivalRestart(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) - <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } l.WaitForCommit(blk.Round()) @@ -416,10 +415,8 @@ func TestArchivalCreatables(t *testing.T) { // Add the block err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - - <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - <-l.Wait(blk.Round()) + l.WaitForCommit(blk.Round()) // check that we can fetch creator for all created assets/apps and // can't for deleted assets/apps @@ -599,9 +596,8 @@ func TestArchivalCreatables(t *testing.T) { blk.Payset = nil err = l.AddBlock(blk, agreement.Certificate{}) require.NoError(t, err) - <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } - <-l.Wait(blk.Round()) + l.WaitForCommit(blk.Round()) // close and reopen the same DB l.Close() diff --git a/ledger/notifier.go b/ledger/notifier.go index bb06b62b93..fa6dd99c50 100644 --- a/ledger/notifier.go +++ b/ledger/notifier.go @@ -81,7 +81,10 @@ func (bn *blockNotifier) close() { bn.cond.Broadcast() } bn.mu.Unlock() - <-bn.closed + // we use select here so that we can read from nil channels - it's being used in our unit testing. + select { + case <-bn.closed: + } } func (bn *blockNotifier) loadFromDisk(l ledgerForTracker) error { From 25c15c3f1a874b5760598a386b294f9c80716fde Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 14:11:48 -0400 Subject: [PATCH 190/267] forgot to remove this one. --- ledger/archival_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index 20ed8135ca..dc1bbac125 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -679,7 +679,6 @@ func TestArchivalFromNonArchival(t *testing.T) { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) l.AddBlock(blk, agreement.Certificate{}) - <-l.Wait(blk.BlockHeader.Round - basics.Round(blk.BlockHeader.Round&31)) } l.WaitForCommit(blk.Round()) From a22bccbca5b99d1cfc7b43f550d23f57d821aeed Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 28 Jul 2020 15:01:53 -0400 Subject: [PATCH 191/267] Add some content to the blocks in TestArchivalFromNonArchival so that old blocks would get delete from disk. Refactor the notifier to use WaitGroup so that it would take the same execution path. --- ledger/archival_test.go | 39 ++++++++++++++++++++++++++++++++++++++- ledger/notifier.go | 17 +++++++---------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index dc1bbac125..3538626a34 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -169,6 +169,9 @@ func TestArchivalRestart(t *testing.T) { // disable deadlock checking code deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = false + }() dbTempDir, err := ioutil.TempDir("", "testdir"+t.Name()) require.NoError(t, err) @@ -658,6 +661,9 @@ func makeSignedTxnInBlock(tx transactions.Transaction) transactions.SignedTxnInB func TestArchivalFromNonArchival(t *testing.T) { // Start in non-archival mode, add 2K blocks, restart in archival mode ensure only genesis block is there deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = false + }() dbTempDir, err := ioutil.TempDir(os.TempDir(), "testdir") require.NoError(t, err) dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) @@ -665,6 +671,23 @@ func TestArchivalFromNonArchival(t *testing.T) { defer os.RemoveAll(dbTempDir) genesisInitState := getInitState() + + genesisInitState.Block.BlockHeader.GenesisHash = crypto.Digest{} + genesisInitState.Block.CurrentProtocol = protocol.ConsensusFuture + genesisInitState.GenesisHash = crypto.Digest{1} + genesisInitState.Block.BlockHeader.GenesisHash = crypto.Digest{1} + + balanceRecords := []basics.BalanceRecord{} + + for i := 0; i < 50; i++ { + addr := basics.Address{} + _, err = rand.Read(addr[:]) + require.NoError(t, err) + br := basics.BalanceRecord{AccountData: basics.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1234567890}), Addr: addr} + genesisInitState.Accounts[addr] = br.AccountData + balanceRecords = append(balanceRecords, br) + } + const inMem = false // use persistent storage cfg := config.GetDefaultLocal() cfg.Archival = false @@ -678,7 +701,21 @@ func TestArchivalFromNonArchival(t *testing.T) { for i := 0; i < maxBlocks; i++ { blk.BlockHeader.Round++ blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000) - l.AddBlock(blk, agreement.Certificate{}) + blk.Payset = transactions.Payset{} + + for j := 0; j < 5; j++ { + x := (j + i) % len(balanceRecords) + creatorEncoded := balanceRecords[x].Addr.String() + tx, err := makeUnsignedAssetCreateTx(blk.BlockHeader.Round-1, blk.BlockHeader.Round+3, 100, false, creatorEncoded, creatorEncoded, creatorEncoded, creatorEncoded, "m", "m", "", nil) + require.NoError(t, err) + tx.Sender = balanceRecords[x].Addr + stxnib := makeSignedTxnInBlock(tx) + blk.Payset = append(blk.Payset, stxnib) + blk.BlockHeader.TxnCounter++ + } + + err := l.AddBlock(blk, agreement.Certificate{}) + require.NoError(t, err) } l.WaitForCommit(blk.Round()) diff --git a/ledger/notifier.go b/ledger/notifier.go index fa6dd99c50..e2f4bff6f1 100644 --- a/ledger/notifier.go +++ b/ledger/notifier.go @@ -41,12 +41,12 @@ type blockNotifier struct { listeners []BlockListener pendingBlocks []blockDeltaPair running bool - // closed is used to syncronize closing the worker goroutine. It's being created during loadFromDisk, and the worker is responsible to close it once it's aborting it's goroutine. The close function waits on this to complete. - closed chan struct{} + // closing is the waitgroup used to syncronize closing the worker goroutine. It's being increased during loadFromDisk, and the worker is responsible to call Done on it once it's aborting it's goroutine. The close function waits on this to complete. + closing sync.WaitGroup } -func (bn *blockNotifier) worker(closed chan struct{}) { - defer close(closed) +func (bn *blockNotifier) worker() { + defer bn.closing.Done() bn.mu.Lock() for { @@ -81,18 +81,15 @@ func (bn *blockNotifier) close() { bn.cond.Broadcast() } bn.mu.Unlock() - // we use select here so that we can read from nil channels - it's being used in our unit testing. - select { - case <-bn.closed: - } + bn.closing.Wait() } func (bn *blockNotifier) loadFromDisk(l ledgerForTracker) error { bn.cond = sync.NewCond(&bn.mu) bn.running = true bn.pendingBlocks = nil - bn.closed = make(chan struct{}) - go bn.worker(bn.closed) + bn.closing.Add(1) + go bn.worker() return nil } From c026e96bf6b1d13bbe9d6092772286e317302315 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Tue, 28 Jul 2020 16:30:12 -0400 Subject: [PATCH 192/267] -- Add tests --- .../kmd/e2e_kmd_wallet_multisig_test.go | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/test/e2e-go/kmd/e2e_kmd_wallet_multisig_test.go b/test/e2e-go/kmd/e2e_kmd_wallet_multisig_test.go index 4c2e70aa13..b54f98a1d2 100644 --- a/test/e2e-go/kmd/e2e_kmd_wallet_multisig_test.go +++ b/test/e2e-go/kmd/e2e_kmd_wallet_multisig_test.go @@ -224,6 +224,141 @@ func TestMultisigSign(t *testing.T) { // require.NoError(t, err) } +func TestMultisigSignWithSigner(t *testing.T) { + t.Parallel() + var f fixtures.KMDFixture + walletHandleToken := f.SetupWithWallet(t) + defer f.Shutdown() + + resp, err := f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pk1 := addrToPK(t, resp.Address) + resp, err = f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pk2 := addrToPK(t, resp.Address) + pk3 := crypto.PublicKey{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} // some public key we haven't imported + + sender, err := f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pkSender := addrToPK(t, sender.Address) + + // Create a 2-of-3 multisig account from the three public keys + resp1, err := f.Client.ImportMultisigAddr([]byte(walletHandleToken), 1, 2, []crypto.PublicKey{pk1, pk2, pk3}) + + require.NoError(t, err) + msigAddr := addrToPK(t, resp1.Address) + + // Make a transaction spending from the multisig address + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: basics.Address(pkSender), + Fee: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinTxnFee}, + FirstValid: basics.Round(1), + LastValid: basics.Round(1), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: basics.Address{}, + Amount: basics.MicroAlgos{}, + }, + } + + // Try to sign + req2 := kmdapi.APIV1POSTMultisigTransactionSignRequest{ + WalletHandleToken: walletHandleToken, + Transaction: protocol.Encode(&tx), + PublicKey: pk1, + PartialMsig: crypto.MultisigSig{ + Threshold: 2, + Version: 1, + Subsigs: []crypto.MultisigSubsig{{Key: pk1}, {Key: pk2}, {Key: pk3}}, + }, + WalletPassword: f.WalletPassword, + AuthAddr: crypto.Digest(msigAddr), + } + resp2 := kmdapi.APIV1POSTMultisigTransactionSignResponse{} + err = f.Client.DoV1Request(req2, &resp2) + require.NoError(t, err) + + var msig crypto.MultisigSig + err = protocol.Decode(resp2.Multisig, &msig) + require.NoError(t, err) + + // Try to add another signature + req3 := kmdapi.APIV1POSTMultisigTransactionSignRequest{ + WalletHandleToken: walletHandleToken, + Transaction: protocol.Encode(&tx), + PublicKey: pk2, + PartialMsig: msig, + WalletPassword: f.WalletPassword, + AuthAddr: crypto.Digest(msigAddr), + } + resp3 := kmdapi.APIV1POSTMultisigTransactionSignResponse{} + err = f.Client.DoV1Request(req3, &resp3) + require.NoError(t, err) + + // Assemble them into a signed transaction and see if it verifies + _, err = transactions.AssembleSignedTxn(tx, crypto.Signature{}, msig) + require.NoError(t, err) + +} + +func TestMultisigSignWithWrongSigner(t *testing.T) { + t.Parallel() + var f fixtures.KMDFixture + walletHandleToken := f.SetupWithWallet(t) + defer f.Shutdown() + + resp, err := f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pk1 := addrToPK(t, resp.Address) + resp, err = f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pk2 := addrToPK(t, resp.Address) + pk3 := crypto.PublicKey{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} // some public key we haven't imported + + sender, err := f.Client.GenerateKey([]byte(walletHandleToken)) + require.NoError(t, err) + pkSender := addrToPK(t, sender.Address) + + // Create a 2-of-3 multisig account from the three public keys + _, err = f.Client.ImportMultisigAddr([]byte(walletHandleToken), 1, 2, []crypto.PublicKey{pk1, pk2, pk3}) + require.NoError(t, err) + + // Make a transaction spending from the multisig address + tx := transactions.Transaction{ + Type: protocol.PaymentTx, + Header: transactions.Header{ + Sender: basics.Address(pkSender), + Fee: basics.MicroAlgos{Raw: config.Consensus[protocol.ConsensusCurrentVersion].MinTxnFee}, + FirstValid: basics.Round(1), + LastValid: basics.Round(1), + }, + PaymentTxnFields: transactions.PaymentTxnFields{ + Receiver: basics.Address{}, + Amount: basics.MicroAlgos{}, + }, + } + + // Try to sign + req2 := kmdapi.APIV1POSTMultisigTransactionSignRequest{ + WalletHandleToken: walletHandleToken, + Transaction: protocol.Encode(&tx), + PublicKey: pk1, + PartialMsig: crypto.MultisigSig{ + Threshold: 2, + Version: 1, + Subsigs: []crypto.MultisigSubsig{{Key: pk1}, {Key: pk2}, {Key: pk3}}, + }, + WalletPassword: f.WalletPassword, + } + + resp2 := kmdapi.APIV1POSTMultisigTransactionSignResponse{} + err = f.Client.DoV1Request(req2, &resp2) + require.Error(t, err) + +} + func TestMultisigSignProgram(t *testing.T) { t.Parallel() var f fixtures.KMDFixture From a1763e55eadafff981e28ebf88072a4e111b7e87 Mon Sep 17 00:00:00 2001 From: Rotem Hemo Date: Tue, 28 Jul 2020 19:01:06 -0400 Subject: [PATCH 193/267] -- Addressing comments --- cmd/goal/clerk.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/goal/clerk.go b/cmd/goal/clerk.go index 5785804b51..c685c07539 100644 --- a/cmd/goal/clerk.go +++ b/cmd/goal/clerk.go @@ -87,7 +87,7 @@ func init() { sendCmd.Flags().StringVarP(&progByteFile, "from-program-bytes", "P", "", "Program binary to use as account logic") sendCmd.Flags().StringSliceVar(&argB64Strings, "argb64", nil, "base64 encoded args to pass to transaction logic") sendCmd.Flags().StringVarP(&logicSigFile, "logic-sig", "L", "", "LogicSig to apply to transaction") - sendCmd.Flags().StringVar(&msigParams, "msig-params", "", "Multisig pre image parameters - [threshold] [Address 1] [Address 2] ...\nUsed to add the necessary fields in case the account was rekeyed to a multisig account") + sendCmd.Flags().StringVar(&msigParams, "msig-params", "", "Multisig preimage parameters - [threshold] [Address 1] [Address 2] ...\nUsed to add the necessary fields in case the account was rekeyed to a multisig account") sendCmd.MarkFlagRequired("to") sendCmd.MarkFlagRequired("amount") @@ -406,7 +406,7 @@ var sendCmd = &cobra.Command{ } threshold, err := strconv.ParseUint(params[0], 10, 8) - if err != nil { + if err != nil || threshold < 1 || threshold > 255 { reportErrorf(msigParseError, "Failed to parse the threshold. Make sure it's a number between 1 and 255") } From 8cd2a328aa926aee5669a0b458f88fd28d877199 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Tue, 28 Jul 2020 19:59:05 -0400 Subject: [PATCH 194/267] Avoid closure. --- daemon/algod/api/server/v2/utils.go | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index f87b333a83..316bc0995c 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -180,26 +180,26 @@ func decode(handle codec.Handle, data []byte, v interface{}) error { return nil } -func convertToDeltas(txn node.TxnWithStatus) (*[]generated.AccountStateDelta, *generated.StateDelta) { - // Helper to convert basics.StateDelta -> *generated.StateDelta - convertDelta := func (d basics.StateDelta) *generated.StateDelta { - if len(d) == 0 { - return nil - } - var delta generated.StateDelta - for k, v:= range d { - delta = append(delta, generated.EvalDeltaKeyValue{ - Key: base64.StdEncoding.EncodeToString([]byte(k)), - Value: generated.EvalDelta{ - Action: uint64(v.Action), - Bytes: strOrNil(base64.StdEncoding.EncodeToString([]byte(v.Bytes))), - Uint: numOrNil(v.Uint), - }, - }) - } - return &delta +// Helper to convert basics.StateDelta -> *generated.StateDelta +func stateDeltaToStateDelta(d basics.StateDelta) *generated.StateDelta { + if len(d) == 0 { + return nil } + var delta generated.StateDelta + for k, v:= range d { + delta = append(delta, generated.EvalDeltaKeyValue{ + Key: base64.StdEncoding.EncodeToString([]byte(k)), + Value: generated.EvalDelta{ + Action: uint64(v.Action), + Bytes: strOrNil(base64.StdEncoding.EncodeToString([]byte(v.Bytes))), + Uint: numOrNil(v.Uint), + }, + }) + } + return &delta +} +func convertToDeltas(txn node.TxnWithStatus) (*[]generated.AccountStateDelta, *generated.StateDelta) { var localStateDelta *[]generated.AccountStateDelta if len(txn.ApplyData.EvalDelta.LocalDeltas) > 0 { d := make([]generated.AccountStateDelta, 0) @@ -219,12 +219,12 @@ func convertToDeltas(txn node.TxnWithStatus) (*[]generated.AccountStateDelta, *g } d = append(d, generated.AccountStateDelta{ Address: addr, - Delta: *(convertDelta(v)), + Delta: *(stateDeltaToStateDelta(v)), }) } localStateDelta = &d } - return localStateDelta, convertDelta(txn.ApplyData.EvalDelta.GlobalDelta) + return localStateDelta, stateDeltaToStateDelta(txn.ApplyData.EvalDelta.GlobalDelta) } From cdd27844a60b7517e34e6060870170c1154c3dfb Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 29 Jul 2020 11:51:44 -0400 Subject: [PATCH 195/267] Add missing error code testing. --- ledger/acctupdates.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index c56009af2c..4300c0a2c8 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -242,6 +242,9 @@ func (au *accountUpdates) loadFromDisk(l ledgerForTracker) error { } writingCatchpointDigest, err = au.initializeCaches(lastBalancesRound, lastestBlockRound, basics.Round(writingCatchpointRound)) + if err != nil { + return err + } if writingCatchpointRound != 0 && au.catchpointInterval != 0 { au.generateCatchpoint(basics.Round(writingCatchpointRound), au.lastCatchpointLabel, writingCatchpointDigest, time.Duration(0)) From ed1a868baff23e59a69fd0f1d7f57def82eac0c9 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 12:35:23 -0400 Subject: [PATCH 196/267] add foreign assets field to app call txns --- cmd/tealdbg/local_test.go | 4 +- config/consensus.go | 8 + data/transactions/application.go | 10 + data/transactions/application_test.go | 3 + data/transactions/logic/README.md | 2 +- data/transactions/logic/TEAL_opcodes.md | 6 +- data/transactions/logic/doc.go | 4 +- data/transactions/logic/eval.go | 36 +- data/transactions/logic/evalStateful_test.go | 109 +- data/transactions/logic/eval_test.go | 1 + data/transactions/logic/opcodes.go | 2 +- data/transactions/msgp_gen.go | 1020 ++++++++++-------- ledger/applications.go | 17 +- ledger/applications_test.go | 4 +- 14 files changed, 700 insertions(+), 526 deletions(-) diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index 66a3d4ad8c..26aa5aa765 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -898,12 +898,10 @@ func TestLocalLedger(t *testing.T) { holdings, err = ledger.AssetHolding(sender, assetIdx+1) a.Error(err) - params, err := ledger.AssetParams(sender, assetIdx) + params, err := ledger.AssetParams(assetIdx) a.NoError(err) a.Equal(uint64(100), params.Total) a.Equal("tok", params.UnitName) - params, err = ledger.AssetParams(payTxn.Txn.Receiver, assetIdx) - a.Error(err) tkv, err := ledger.AppGlobalState(0) a.NoError(err) diff --git a/config/consensus.go b/config/consensus.go index e9892d85a4..467f904238 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -237,6 +237,11 @@ type ConsensusParams struct { // which global state may be read in the transaction MaxAppTxnForeignApps int + // maximum number of asset ids in the ApplicationCall ForeignAssets + // field. these are the only assets for which the asset parameters may + // be read in the transaction + MaxAppTxnForeignAssets int + // maximum cost of application approval program or clear state program MaxAppProgramCost int @@ -717,6 +722,9 @@ func initConsensusProtocols() { // Can look up 2 other app creator balance records to see global state vFuture.MaxAppTxnForeignApps = 2 + // Can look up 2 assets to see asset parameters + vFuture.MaxAppTxnForeignAssets = 2 + // 64 byte keys @ ~333 microAlgos/byte + delta vFuture.SchemaMinBalancePerEntry = 25000 diff --git a/data/transactions/application.go b/data/transactions/application.go index 58f35299ed..e08b7decbb 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -40,6 +40,12 @@ const ( // contain. Its value is verified against consensus parameters in // TestEncodedAppTxnAllocationBounds encodedMaxForeignApps = 32 + + // encodedMaxForeignAssets sets the allocation bound for the maximum + // number of ForeignAssets that a transaction decoded off of the wire + // can contain. Its value is verified against consensus parameters in + // TestEncodedAppTxnAllocationBounds + encodedMaxForeignAssets = 32 ) // OnCompletion is an enum representing some layer 1 side effect that an @@ -107,6 +113,10 @@ type ApplicationCallTxnFields struct { // ClearStateProgram. ForeignApps []basics.AppIndex `codec:"apfa,allocbound=encodedMaxForeignApps"` + // ForeignAssets are asset IDs for assets whose AssetParams may be read + // by the executing ApprovalProgram or ClearStateProgram. + ForeignAssets []basics.AssetIndex `codec:"apas,allocbound=encodedMaxForeignAssets"` + // LocalStateSchema specifies the maximum number of each type that may // appear in the local key/value store of users who opt in to this // application. This field is only used during application creation diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 9853897ca0..8e9cd0616a 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -1396,5 +1396,8 @@ func TestEncodedAppTxnAllocationBounds(t *testing.T) { if proto.MaxAppTxnForeignApps > encodedMaxForeignApps { require.Failf(t, "proto.MaxAppTxnForeignApps > encodedMaxForeignApps", "protocol version = %s", protoVer) } + if proto.MaxAppTxnForeignAssets > encodedMaxForeignAssets { + require.Failf(t, "proto.MaxAppTxnForeignAssets > encodedMaxForeignAssets", "protocol version = %s", protoVer) + } } } diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 4fb4f9a8d6..5f0a3e56a4 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -291,7 +291,7 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as | `app_local_del` | delete from account specified by Txn.Accounts[A] local state key B of the current application | | `app_global_del` | delete key A from a global state of the current application | | `asset_holding_get` | read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value} | -| `asset_params_get` | read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value} | +| `asset_params_get` | read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value} | # Assembler Syntax diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index d1d81b820f..55a95e10bf 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -699,9 +699,9 @@ params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherw ## asset_params_get - Opcode: 0x71 {uint8 asset params field index} -- Pops: *... stack*, {uint64 A}, {uint64 B} +- Pops: *... stack*, uint64 - Pushes: uint64, any -- read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value} +- read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value} - LogicSigVersion >= 2 - Mode: Application @@ -722,4 +722,4 @@ params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherw | 10 | AssetClawback | []byte | Clawback address | -params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value. +params: txn.ForeignAssets offset. Return: did_exist flag (1 if exist and 0 otherwise), value. diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index ded987a192..83133d3bd7 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -110,7 +110,7 @@ var opDocList = []stringString{ {"app_local_del", "delete from account specified by Txn.Accounts[A] local state key B of the current application"}, {"app_global_del", "delete key A from a global state of the current application"}, {"asset_holding_get", "read from account specified by Txn.Accounts[A] and asset B holding field X (imm arg) => {0 or 1 (top), value}"}, - {"asset_params_get", "read from account specified by Txn.Accounts[A] and asset B params field X (imm arg) => {0 or 1 (top), value}"}, + {"asset_params_get", "read from asset Txn.ForeignAssets[A] params field X (imm arg) => {0 or 1 (top), value}"}, } var opDocByName map[string]string @@ -177,7 +177,7 @@ var opDocExtraList = []stringString{ {"app_local_del", "params: account index, state key."}, {"app_global_del", "params: state key."}, {"asset_holding_get", "params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value."}, - {"asset_params_get", "params: account index, asset id. Return: did_exist flag (1 if exist and 0 otherwise), value."}, + {"asset_params_get", "params: txn.ForeignAssets offset. Return: did_exist flag (1 if exist and 0 otherwise), value."}, } var opDocExtras map[string]string diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index b5341e0f0a..3553c3d1f5 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -106,7 +106,7 @@ type LedgerForLogic interface { AppGlobalState(appIdx basics.AppIndex) (basics.TealKeyValue, error) AppLocalState(addr basics.Address, appIdx basics.AppIndex) (basics.TealKeyValue, error) AssetHolding(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetHolding, error) - AssetParams(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetParams, error) + AssetParams(assetIdx basics.AssetIndex) (basics.AssetParams, error) ApplicationID() basics.AppIndex LocalSchema() basics.StateSchema GlobalSchema() basics.StateSchema @@ -586,12 +586,16 @@ func (cx *evalContext) step() { if len(spec.Returns) > 1 { num = len(spec.Returns) } - if len(cx.stack) < num { - cx.err = fmt.Errorf("stack underflow: expected %d, have %d", num, len(cx.stack)) - return - } - for i := 1; i <= num; i++ { - stackString += fmt.Sprintf("(%s) ", cx.stack[len(cx.stack)-i].String()) + // check for nil error here, because we might not return + // values if we encounter an error in the opcode + if cx.err == nil { + if len(cx.stack) < num { + cx.err = fmt.Errorf("stack underflow: expected %d, have %d", num, len(cx.stack)) + return + } + for i := 1; i <= num; i++ { + stackString += fmt.Sprintf("(%s) ", cx.stack[len(cx.stack)-i].String()) + } } } fmt.Fprintf(cx.Trace, "%3d %s%s=> %s\n", cx.pc, spec.Name, immArgsString, stackString) @@ -2137,11 +2141,9 @@ func opAssetHoldingGet(cx *evalContext) { } func opAssetParamsGet(cx *evalContext) { - last := len(cx.stack) - 1 // asset id - prev := last - 1 // account offset + last := len(cx.stack) - 1 // foreign asset id - assetID := cx.stack[last].Uint - accountIdx := cx.stack[prev].Uint + foreignAssetsIndex := cx.stack[last].Uint paramIdx := uint64(cx.program[cx.pc+1]) if cx.Ledger == nil { @@ -2149,15 +2151,15 @@ func opAssetParamsGet(cx *evalContext) { return } - addr, err := cx.Txn.Txn.AddressByIndex(accountIdx, cx.Txn.Txn.Sender) - if err != nil { - cx.err = err + if foreignAssetsIndex >= uint64(len(cx.Txn.Txn.ForeignAssets)) { + cx.err = fmt.Errorf("invalid ForeignAssets index %d", foreignAssetsIndex) return } + assetID := cx.Txn.Txn.ForeignAssets[foreignAssetsIndex] var exist uint64 = 0 var value stackValue - if params, err := cx.Ledger.AssetParams(addr, basics.AssetIndex(assetID)); err == nil { + if params, err := cx.Ledger.AssetParams(basics.AssetIndex(assetID)); err == nil { // params exist, read the value exist = 1 value, err = cx.assetParamsEnumToValue(¶ms, paramIdx) @@ -2167,8 +2169,8 @@ func opAssetParamsGet(cx *evalContext) { } } - cx.stack[prev] = value - cx.stack[last].Uint = exist + cx.stack[last] = value + cx.stack = append(cx.stack, stackValue{Uint: exist}) cx.nextpc = cx.pc + 2 } diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 207af52e55..fa411b4d2e 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -34,12 +34,12 @@ type balanceRecord struct { balance uint64 apps map[basics.AppIndex]map[string]basics.TealValue holdings map[uint64]basics.AssetHolding - assets map[uint64]basics.AssetParams } type testLedger struct { balances map[basics.Address]balanceRecord applications map[basics.AppIndex]map[string]basics.TealValue + assets map[basics.AssetIndex]basics.AssetParams localCount int globalCount int appID uint64 @@ -51,7 +51,6 @@ func makeBalanceRecord(addr basics.Address, balance uint64) balanceRecord { balance: balance, apps: make(map[basics.AppIndex]map[string]basics.TealValue), holdings: make(map[uint64]basics.AssetHolding), - assets: make(map[uint64]basics.AssetParams), } return br } @@ -63,6 +62,7 @@ func makeTestLedger(balances map[basics.Address]uint64) *testLedger { l.balances[addr] = makeBalanceRecord(addr, balance) } l.applications = make(map[basics.AppIndex]map[string]basics.TealValue) + l.assets = make(map[basics.AssetIndex]basics.AssetParams) return l } @@ -83,13 +83,8 @@ func (l *testLedger) newApp(addr basics.Address, appID uint64) { l.balances[addr] = br } -func (l *testLedger) setAsset(addr basics.Address, assetID uint64, params basics.AssetParams) { - br, ok := l.balances[addr] - if !ok { - br = makeBalanceRecord(addr, 0) - } - br.assets[assetID] = params - l.balances[addr] = br +func (l *testLedger) newAsset(assetID uint64, params basics.AssetParams) { + l.assets[basics.AssetIndex(assetID)] = params } func (l *testLedger) setHolding(addr basics.Address, assetID uint64, holding basics.AssetHolding) { @@ -157,14 +152,11 @@ func (l *testLedger) AssetHolding(addr basics.Address, assetID basics.AssetIndex return basics.AssetHolding{}, fmt.Errorf("no such address") } -func (l *testLedger) AssetParams(addr basics.Address, assetID basics.AssetIndex) (basics.AssetParams, error) { - if br, ok := l.balances[addr]; ok { - if asset, ok := br.assets[uint64(assetID)]; ok { - return asset, nil - } - return basics.AssetParams{}, fmt.Errorf("No asset for account") +func (l *testLedger) AssetParams(assetID basics.AssetIndex) (basics.AssetParams, error) { + if asset, ok := l.assets[assetID]; ok { + return asset, nil } - return basics.AssetParams{}, fmt.Errorf("no such address") + return basics.AssetParams{}, fmt.Errorf("No such asset") } func (l *testLedger) ApplicationID() basics.AppIndex { @@ -299,7 +291,6 @@ asset_holding_get AssetBalance pop && intc_0 -intc 5 asset_params_get AssetTotal pop && @@ -356,7 +347,7 @@ pop ) ledger.newApp(txn.Txn.Sender, 100) ledger.balances[txn.Txn.Sender].apps[100]["ALGO"] = algoValue - ledger.setAsset(txn.Txn.Receiver, 5, params) + ledger.newAsset(5, params) ledger.setHolding(txn.Txn.Sender, 5, basics.AssetHolding{Amount: 123, Frozen: true}) for mode, test := range tests { @@ -924,40 +915,35 @@ bnz error int 1 == && -int 1 -int 55 +int 0 asset_params_get AssetTotal ! bnz error int 1000 == && -int 1 -int 55 +int 0 asset_params_get AssetDecimals ! bnz error int 2 == && -int 1 -int 55 +int 0 asset_params_get AssetDefaultFrozen ! bnz error int 0 == && -int 1 -int 55 +int 0 asset_params_get AssetUnitName ! bnz error byte 0x414c474f == && -int 1 -int 55 +int 0 asset_params_get AssetName ! bnz error @@ -965,48 +951,42 @@ len int 0 == && -int 1 -int 55 +int 0 asset_params_get AssetURL ! bnz error txna ApplicationArgs 0 == && -int 1 -int 55 +int 0 asset_params_get AssetMetadataHash ! bnz error byte 0x0000000000000000000000000000000000000000000000000000000000000000 == && -int 1 -int 55 +int 0 asset_params_get AssetManager ! bnz error txna Accounts 0 == && -int 1 -int 55 +int 0 asset_params_get AssetReserve ! bnz error txna Accounts 1 == && -int 1 -int 55 +int 0 asset_params_get AssetFreeze ! bnz error txna Accounts 1 == && -int 1 -int 55 +int 0 asset_params_get AssetClawback ! bnz error @@ -1033,14 +1013,17 @@ func TestAssets(t *testing.T) { } } + type sourceError struct { + src string + err string + } // check generic errors - sources := []string{ - "int 5\nint 55\nasset_holding_get AssetBalance", - "int 5\nint 55\nasset_params_get AssetTotal", + sources := []sourceError{ + sourceError{"int 5\nint 55\nasset_holding_get AssetBalance", "cannot load account[5]"}, + sourceError{"int 5\nasset_params_get AssetTotal", "invalid ForeignAssets index 5"}, } - for _, source := range sources { - - program, err := AssembleStringWithVersion(source, AssemblerMaxVersion) + for _, sourceErr := range sources { + program, err := AssembleStringWithVersion(sourceErr.src, AssemblerMaxVersion) require.NoError(t, err) txn := makeSampleTxn() @@ -1062,7 +1045,7 @@ func TestAssets(t *testing.T) { _, _, err = EvalStateful(program, ep) require.Error(t, err) - require.Contains(t, err.Error(), "cannot load account[5]") + require.Contains(t, err.Error(), sourceErr.err) } program, err := AssembleStringWithVersion(assetsTestProgram, AssemblerMaxVersion) @@ -1090,7 +1073,7 @@ func TestAssets(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.newAsset(55, params) ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) ep := defaultEvalParams(&sb, &txn) @@ -1138,8 +1121,7 @@ int 1 require.Contains(t, err.Error(), "invalid asset holding field 2") // check holdings bool value - source = `int 1 -int 55 + source = `int 0 asset_params_get AssetDefaultFrozen ! bnz error @@ -1154,21 +1136,21 @@ int 1 program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.DefaultFrozen = true - ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.newAsset(55, params) pass, _, err = EvalStateful(program, ep) require.NoError(t, err) require.True(t, pass) - + t.Logf("%x", program) // check holdings invalid offsets - require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, program[7]) - program[8] = 0x20 + require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, program[6]) + program[7] = 0x20 + t.Logf("%x", program) _, _, err = EvalStateful(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid asset params field 32") // check empty string - source = `int 1 // account idx (txn.Accounts[1]) -int 55 + source = `int 0 // foreign asset idx (txn.ForeignAssets[0]) asset_params_get AssetURL ! bnz error @@ -1184,13 +1166,12 @@ int 1 program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" - ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.newAsset(55, params) pass, _, err = EvalStateful(program, ep) require.NoError(t, err) require.True(t, pass) - source = `int 1 -int 55 + source = `int 0 asset_params_get AssetURL ! bnz error @@ -1205,7 +1186,7 @@ int 1 program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) params.URL = "" - ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.newAsset(55, params) cost, err = CheckStateful(program, ep) require.NoError(t, err) require.True(t, cost < 1000) @@ -2450,7 +2431,7 @@ func TestEnumFieldErrors(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.setAsset(txn.Txn.Receiver, 55, params) + ledger.newAsset(55, params) ledger.setHolding(txn.Txn.Sender, 55, basics.AssetHolding{Amount: 123, Frozen: true}) ep.Txn = &txn @@ -2473,8 +2454,7 @@ pop require.Error(t, err) require.Contains(t, err.Error(), "AssetBalance expected field type is []byte but got uint64") - source = `int 1 -int 55 + source = `int 0 asset_params_get AssetTotal pop ` @@ -2506,6 +2486,7 @@ func TestReturnTypes(t *testing.T) { ep.TxnGroup = txgroup ep.Txn.Txn.ApplicationID = 1 ep.Txn.Txn.ForeignApps = []basics.AppIndex{txn.Txn.ApplicationID} + ep.Txn.Txn.ForeignAssets = []basics.AssetIndex{basics.AssetIndex(1), basics.AssetIndex(1)} txn.Lsig.Args = [][]byte{ []byte("aoeu"), []byte("aoeu"), @@ -2529,7 +2510,7 @@ func TestReturnTypes(t *testing.T) { Freeze: txn.Txn.Receiver, Clawback: txn.Txn.Receiver, } - ledger.setAsset(txn.Txn.Receiver, 1, params) + ledger.newAsset(1, params) ledger.setHolding(txn.Txn.Sender, 1, basics.AssetHolding{Amount: 123, Frozen: true}) ledger.newApp(txn.Txn.Sender, 1) ledger.balances[txn.Txn.Receiver] = makeBalanceRecord(txn.Txn.Receiver, 1) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 314f4318d9..101e9e089e 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -1649,6 +1649,7 @@ func makeSampleTxn() transactions.SignedTxn { txn.Txn.FreezeAsset = 34 copy(txn.Txn.FreezeAccount[:], freezeAccAddr) txn.Txn.AssetFrozen = true + txn.Txn.ForeignAssets = []basics.AssetIndex{55} return txn } diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index a2dbc29ef5..3575921200 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -158,7 +158,7 @@ var OpSpecs = []OpSpec{ {0x69, "app_global_del", opAppDeleteGlobalState, asmDefault, disDefault, oneBytes, nil, 2, runModeApplication, opSizeDefault}, {0x70, "asset_holding_get", opAssetHoldingGet, assembleAssetHolding, disAssetHolding, twoInts, oneInt.plus(oneAny), 2, runModeApplication, opSize{1, 2, nil}}, - {0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, twoInts, oneInt.plus(oneAny), 2, runModeApplication, opSize{1, 2, nil}}, + {0x71, "asset_params_get", opAssetParamsGet, assembleAssetParams, disAssetParams, oneInt, oneInt.plus(oneAny), 2, runModeApplication, opSize{1, 2, nil}}, } type sortByOpcode []OpSpec diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index a046aaf7ae..beeeaa6008 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -159,48 +159,52 @@ import ( func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0004Len := uint32(9) - var zb0004Mask uint16 /* 10 bits */ + zb0005Len := uint32(10) + var zb0005Mask uint16 /* 11 bits */ if len((*z).ApplicationArgs) == 0 { - zb0004Len-- - zb0004Mask |= 0x2 + zb0005Len-- + zb0005Mask |= 0x2 } if (*z).OnCompletion == 0 { - zb0004Len-- - zb0004Mask |= 0x4 + zb0005Len-- + zb0005Mask |= 0x4 } if len((*z).ApprovalProgram) == 0 { - zb0004Len-- - zb0004Mask |= 0x8 + zb0005Len-- + zb0005Mask |= 0x8 + } + if len((*z).ForeignAssets) == 0 { + zb0005Len-- + zb0005Mask |= 0x10 } if len((*z).Accounts) == 0 { - zb0004Len-- - zb0004Mask |= 0x10 + zb0005Len-- + zb0005Mask |= 0x20 } if len((*z).ForeignApps) == 0 { - zb0004Len-- - zb0004Mask |= 0x20 + zb0005Len-- + zb0005Mask |= 0x40 } if (*z).GlobalStateSchema.MsgIsZero() { - zb0004Len-- - zb0004Mask |= 0x40 + zb0005Len-- + zb0005Mask |= 0x80 } if (*z).ApplicationID.MsgIsZero() { - zb0004Len-- - zb0004Mask |= 0x80 + zb0005Len-- + zb0005Mask |= 0x100 } if (*z).LocalStateSchema.MsgIsZero() { - zb0004Len-- - zb0004Mask |= 0x100 + zb0005Len-- + zb0005Mask |= 0x200 } if len((*z).ClearStateProgram) == 0 { - zb0004Len-- - zb0004Mask |= 0x200 + zb0005Len-- + zb0005Mask |= 0x400 } - // variable map header, size zb0004Len - o = append(o, 0x80|uint8(zb0004Len)) - if zb0004Len != 0 { - if (zb0004Mask & 0x2) == 0 { // if not empty + // variable map header, size zb0005Len + o = append(o, 0x80|uint8(zb0005Len)) + if zb0005Len != 0 { + if (zb0005Mask & 0x2) == 0 { // if not empty // string "apaa" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x61) if (*z).ApplicationArgs == nil { @@ -212,17 +216,33 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.AppendBytes(o, (*z).ApplicationArgs[zb0001]) } } - if (zb0004Mask & 0x4) == 0 { // if not empty + if (zb0005Mask & 0x4) == 0 { // if not empty // string "apan" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x6e) o = msgp.AppendUint64(o, uint64((*z).OnCompletion)) } - if (zb0004Mask & 0x8) == 0 { // if not empty + if (zb0005Mask & 0x8) == 0 { // if not empty // string "apap" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x70) o = msgp.AppendBytes(o, (*z).ApprovalProgram) } - if (zb0004Mask & 0x10) == 0 { // if not empty + if (zb0005Mask & 0x10) == 0 { // if not empty + // string "apas" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x73) + if (*z).ForeignAssets == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ForeignAssets))) + } + for zb0004 := range (*z).ForeignAssets { + o, err = (*z).ForeignAssets[zb0004].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets", zb0004) + return + } + } + } + if (zb0005Mask & 0x20) == 0 { // if not empty // string "apat" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x74) if (*z).Accounts == nil { @@ -238,7 +258,7 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0004Mask & 0x20) == 0 { // if not empty + if (zb0005Mask & 0x40) == 0 { // if not empty // string "apfa" o = append(o, 0xa4, 0x61, 0x70, 0x66, 0x61) if (*z).ForeignApps == nil { @@ -254,7 +274,7 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0004Mask & 0x40) == 0 { // if not empty + if (zb0005Mask & 0x80) == 0 { // if not empty // string "apgs" o = append(o, 0xa4, 0x61, 0x70, 0x67, 0x73) o, err = (*z).GlobalStateSchema.MarshalMsg(o) @@ -263,7 +283,7 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0004Mask & 0x80) == 0 { // if not empty + if (zb0005Mask & 0x100) == 0 { // if not empty // string "apid" o = append(o, 0xa4, 0x61, 0x70, 0x69, 0x64) o, err = (*z).ApplicationID.MarshalMsg(o) @@ -272,7 +292,7 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0004Mask & 0x100) == 0 { // if not empty + if (zb0005Mask & 0x200) == 0 { // if not empty // string "apls" o = append(o, 0xa4, 0x61, 0x70, 0x6c, 0x73) o, err = (*z).LocalStateSchema.MarshalMsg(o) @@ -281,7 +301,7 @@ func (z *ApplicationCallTxnFields) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0004Mask & 0x200) == 0 { // if not empty + if (zb0005Mask & 0x400) == 0 { // if not empty // string "apsu" o = append(o, 0xa4, 0x61, 0x70, 0x73, 0x75) o = msgp.AppendBytes(o, (*z).ClearStateProgram) @@ -299,55 +319,55 @@ func (_ *ApplicationCallTxnFields) CanMarshalMsg(z interface{}) bool { func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0004 int - var zb0005 bool - zb0004, zb0005, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0005 int + var zb0006 bool + zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0004, zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0004 > 0 { - zb0004-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).ApplicationID.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApplicationID") return } } - if zb0004 > 0 { - zb0004-- + if zb0005 > 0 { + zb0005-- { - var zb0006 uint64 - zb0006, bts, err = msgp.ReadUint64Bytes(bts) + var zb0007 uint64 + zb0007, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "OnCompletion") return } - (*z).OnCompletion = OnCompletion(zb0006) + (*z).OnCompletion = OnCompletion(zb0007) } } - if zb0004 > 0 { - zb0004-- - var zb0007 int - var zb0008 bool - zb0007, zb0008, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0005 > 0 { + zb0005-- + var zb0008 int + var zb0009 bool + zb0008, zb0009, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0007 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0007), uint64(encodedMaxApplicationArgs)) + if zb0008 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0008), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0008 { + if zb0009 { (*z).ApplicationArgs = nil - } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0007 { - (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0007] + } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0008 { + (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0008] } else { - (*z).ApplicationArgs = make([][]byte, zb0007) + (*z).ApplicationArgs = make([][]byte, zb0008) } for zb0001 := range (*z).ApplicationArgs { (*z).ApplicationArgs[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationArgs[zb0001]) @@ -357,26 +377,26 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } } - if zb0004 > 0 { - zb0004-- - var zb0009 int - var zb0010 bool - zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0005 > 0 { + zb0005-- + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0009 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxAccounts)) + if zb0010 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0010 { + if zb0011 { (*z).Accounts = nil - } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0009 { - (*z).Accounts = ((*z).Accounts)[:zb0009] + } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0010 { + (*z).Accounts = ((*z).Accounts)[:zb0010] } else { - (*z).Accounts = make([]basics.Address, zb0009) + (*z).Accounts = make([]basics.Address, zb0010) } for zb0002 := range (*z).Accounts { bts, err = (*z).Accounts[zb0002].UnmarshalMsg(bts) @@ -386,26 +406,26 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } } - if zb0004 > 0 { - zb0004-- - var zb0011 int - var zb0012 bool - zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0005 > 0 { + zb0005-- + var zb0012 int + var zb0013 bool + zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0011 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxForeignApps)) + if zb0012 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0012 { + if zb0013 { (*z).ForeignApps = nil - } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0011 { - (*z).ForeignApps = ((*z).ForeignApps)[:zb0011] + } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0012 { + (*z).ForeignApps = ((*z).ForeignApps)[:zb0012] } else { - (*z).ForeignApps = make([]basics.AppIndex, zb0011) + (*z).ForeignApps = make([]basics.AppIndex, zb0012) } for zb0003 := range (*z).ForeignApps { bts, err = (*z).ForeignApps[zb0003].UnmarshalMsg(bts) @@ -415,32 +435,61 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } } - if zb0004 > 0 { - zb0004-- + if zb0005 > 0 { + zb0005-- + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets") + return + } + if zb0014 > encodedMaxForeignAssets { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxForeignAssets)) + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets") + return + } + if zb0015 { + (*z).ForeignAssets = nil + } else if (*z).ForeignAssets != nil && cap((*z).ForeignAssets) >= zb0014 { + (*z).ForeignAssets = ((*z).ForeignAssets)[:zb0014] + } else { + (*z).ForeignAssets = make([]basics.AssetIndex, zb0014) + } + for zb0004 := range (*z).ForeignAssets { + bts, err = (*z).ForeignAssets[zb0004].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets", zb0004) + return + } + } + } + if zb0005 > 0 { + zb0005-- bts, err = (*z).LocalStateSchema.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } } - if zb0004 > 0 { - zb0004-- + if zb0005 > 0 { + zb0005-- bts, err = (*z).GlobalStateSchema.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } } - if zb0004 > 0 { - zb0004-- - var zb0013 int - zb0013, err = msgp.ReadBytesBytesHeader(bts) + if zb0005 > 0 { + zb0005-- + var zb0016 int + zb0016, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") return } - if zb0013 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0013), uint64(config.MaxAppProgramLen)) + if zb0016 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0016), uint64(config.MaxAppProgramLen)) return } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) @@ -449,16 +498,16 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } } - if zb0004 > 0 { - zb0004-- - var zb0014 int - zb0014, err = msgp.ReadBytesBytesHeader(bts) + if zb0005 > 0 { + zb0005-- + var zb0017 int + zb0017, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") return } - if zb0014 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0014), uint64(config.MaxAppProgramLen)) + if zb0017 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxAppProgramLen)) return } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) @@ -467,8 +516,8 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } } - if zb0004 > 0 { - err = msgp.ErrTooManyArrayFields(zb0004) + if zb0005 > 0 { + err = msgp.ErrTooManyArrayFields(zb0005) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -479,11 +528,11 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error err = msgp.WrapError(err) return } - if zb0005 { + if zb0006 { (*z) = ApplicationCallTxnFields{} } - for zb0004 > 0 { - zb0004-- + for zb0005 > 0 { + zb0005-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) @@ -498,33 +547,33 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } case "apan": { - var zb0015 uint64 - zb0015, bts, err = msgp.ReadUint64Bytes(bts) + var zb0018 uint64 + zb0018, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "OnCompletion") return } - (*z).OnCompletion = OnCompletion(zb0015) + (*z).OnCompletion = OnCompletion(zb0018) } case "apaa": - var zb0016 int - var zb0017 bool - zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0019 int + var zb0020 bool + zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0016 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxApplicationArgs)) + if zb0019 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0017 { + if zb0020 { (*z).ApplicationArgs = nil - } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0016 { - (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0016] + } else if (*z).ApplicationArgs != nil && cap((*z).ApplicationArgs) >= zb0019 { + (*z).ApplicationArgs = ((*z).ApplicationArgs)[:zb0019] } else { - (*z).ApplicationArgs = make([][]byte, zb0016) + (*z).ApplicationArgs = make([][]byte, zb0019) } for zb0001 := range (*z).ApplicationArgs { (*z).ApplicationArgs[zb0001], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationArgs[zb0001]) @@ -534,24 +583,24 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } case "apat": - var zb0018 int - var zb0019 bool - zb0018, zb0019, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0021 int + var zb0022 bool + zb0021, zb0022, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Accounts") return } - if zb0018 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0018), uint64(encodedMaxAccounts)) + if zb0021 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "Accounts") return } - if zb0019 { + if zb0022 { (*z).Accounts = nil - } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0018 { - (*z).Accounts = ((*z).Accounts)[:zb0018] + } else if (*z).Accounts != nil && cap((*z).Accounts) >= zb0021 { + (*z).Accounts = ((*z).Accounts)[:zb0021] } else { - (*z).Accounts = make([]basics.Address, zb0018) + (*z).Accounts = make([]basics.Address, zb0021) } for zb0002 := range (*z).Accounts { bts, err = (*z).Accounts[zb0002].UnmarshalMsg(bts) @@ -561,24 +610,24 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error } } case "apfa": - var zb0020 int - var zb0021 bool - zb0020, zb0021, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0023 int + var zb0024 bool + zb0023, zb0024, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ForeignApps") return } - if zb0020 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0020), uint64(encodedMaxForeignApps)) + if zb0023 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0023), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "ForeignApps") return } - if zb0021 { + if zb0024 { (*z).ForeignApps = nil - } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0020 { - (*z).ForeignApps = ((*z).ForeignApps)[:zb0020] + } else if (*z).ForeignApps != nil && cap((*z).ForeignApps) >= zb0023 { + (*z).ForeignApps = ((*z).ForeignApps)[:zb0023] } else { - (*z).ForeignApps = make([]basics.AppIndex, zb0020) + (*z).ForeignApps = make([]basics.AppIndex, zb0023) } for zb0003 := range (*z).ForeignApps { bts, err = (*z).ForeignApps[zb0003].UnmarshalMsg(bts) @@ -587,6 +636,33 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } } + case "apas": + var zb0025 int + var zb0026 bool + zb0025, zb0026, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets") + return + } + if zb0025 > encodedMaxForeignAssets { + err = msgp.ErrOverflow(uint64(zb0025), uint64(encodedMaxForeignAssets)) + err = msgp.WrapError(err, "ForeignAssets") + return + } + if zb0026 { + (*z).ForeignAssets = nil + } else if (*z).ForeignAssets != nil && cap((*z).ForeignAssets) >= zb0025 { + (*z).ForeignAssets = ((*z).ForeignAssets)[:zb0025] + } else { + (*z).ForeignAssets = make([]basics.AssetIndex, zb0025) + } + for zb0004 := range (*z).ForeignAssets { + bts, err = (*z).ForeignAssets[zb0004].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets", zb0004) + return + } + } case "apls": bts, err = (*z).LocalStateSchema.UnmarshalMsg(bts) if err != nil { @@ -600,14 +676,14 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } case "apap": - var zb0022 int - zb0022, err = msgp.ReadBytesBytesHeader(bts) + var zb0027 int + zb0027, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "ApprovalProgram") return } - if zb0022 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0022), uint64(config.MaxAppProgramLen)) + if zb0027 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0027), uint64(config.MaxAppProgramLen)) return } (*z).ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApprovalProgram) @@ -616,14 +692,14 @@ func (z *ApplicationCallTxnFields) UnmarshalMsg(bts []byte) (o []byte, err error return } case "apsu": - var zb0023 int - zb0023, err = msgp.ReadBytesBytesHeader(bts) + var zb0028 int + zb0028, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "ClearStateProgram") return } - if zb0023 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0023), uint64(config.MaxAppProgramLen)) + if zb0028 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0028), uint64(config.MaxAppProgramLen)) return } (*z).ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ClearStateProgram) @@ -663,13 +739,17 @@ func (z *ApplicationCallTxnFields) Msgsize() (s int) { for zb0003 := range (*z).ForeignApps { s += (*z).ForeignApps[zb0003].Msgsize() } + s += 5 + msgp.ArrayHeaderSize + for zb0004 := range (*z).ForeignAssets { + s += (*z).ForeignAssets[zb0004].Msgsize() + } s += 5 + (*z).LocalStateSchema.Msgsize() + 5 + (*z).GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ClearStateProgram) return } // MsgIsZero returns whether this is a zero value func (z *ApplicationCallTxnFields) MsgIsZero() bool { - return ((*z).ApplicationID.MsgIsZero()) && ((*z).OnCompletion == 0) && (len((*z).ApplicationArgs) == 0) && (len((*z).Accounts) == 0) && (len((*z).ForeignApps) == 0) && ((*z).LocalStateSchema.MsgIsZero()) && ((*z).GlobalStateSchema.MsgIsZero()) && (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) + return ((*z).ApplicationID.MsgIsZero()) && ((*z).OnCompletion == 0) && (len((*z).ApplicationArgs) == 0) && (len((*z).Accounts) == 0) && (len((*z).ForeignApps) == 0) && (len((*z).ForeignAssets) == 0) && ((*z).LocalStateSchema.MsgIsZero()) && ((*z).GlobalStateSchema.MsgIsZero()) && (len((*z).ApprovalProgram) == 0) && (len((*z).ClearStateProgram) == 0) } // MarshalMsg implements msgp.Marshaler @@ -3581,173 +3661,177 @@ func (z *SignedTxnWithAD) MsgIsZero() bool { func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0005Len := uint32(39) - var zb0005Mask uint64 /* 47 bits */ + zb0006Len := uint32(40) + var zb0006Mask uint64 /* 48 bits */ if (*z).AssetTransferTxnFields.AssetAmount == 0 { - zb0005Len-- - zb0005Mask |= 0x100 + zb0006Len-- + zb0006Mask |= 0x100 } if (*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x200 + zb0006Len-- + zb0006Mask |= 0x200 } if (*z).AssetFreezeTxnFields.AssetFrozen == false { - zb0005Len-- - zb0005Mask |= 0x400 + zb0006Len-- + zb0006Mask |= 0x400 } if (*z).PaymentTxnFields.Amount.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x800 + zb0006Len-- + zb0006Mask |= 0x800 } if len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0 { - zb0005Len-- - zb0005Mask |= 0x1000 + zb0006Len-- + zb0006Mask |= 0x1000 } if (*z).ApplicationCallTxnFields.OnCompletion == 0 { - zb0005Len-- - zb0005Mask |= 0x2000 + zb0006Len-- + zb0006Mask |= 0x2000 } if len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0 { - zb0005Len-- - zb0005Mask |= 0x4000 + zb0006Len-- + zb0006Mask |= 0x4000 } if (*z).AssetConfigTxnFields.AssetParams.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x8000 + zb0006Len-- + zb0006Mask |= 0x8000 + } + if len((*z).ApplicationCallTxnFields.ForeignAssets) == 0 { + zb0006Len-- + zb0006Mask |= 0x10000 } if len((*z).ApplicationCallTxnFields.Accounts) == 0 { - zb0005Len-- - zb0005Mask |= 0x10000 + zb0006Len-- + zb0006Mask |= 0x20000 } if len((*z).ApplicationCallTxnFields.ForeignApps) == 0 { - zb0005Len-- - zb0005Mask |= 0x20000 + zb0006Len-- + zb0006Mask |= 0x40000 } if (*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x40000 + zb0006Len-- + zb0006Mask |= 0x80000 } if (*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x80000 + zb0006Len-- + zb0006Mask |= 0x100000 } if (*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x100000 + zb0006Len-- + zb0006Mask |= 0x200000 } if len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0 { - zb0005Len-- - zb0005Mask |= 0x200000 + zb0006Len-- + zb0006Mask |= 0x400000 } if (*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x400000 + zb0006Len-- + zb0006Mask |= 0x800000 } if (*z).AssetTransferTxnFields.AssetSender.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x800000 + zb0006Len-- + zb0006Mask |= 0x1000000 } if (*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x1000000 + zb0006Len-- + zb0006Mask |= 0x2000000 } if (*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x2000000 + zb0006Len-- + zb0006Mask |= 0x4000000 } if (*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x4000000 + zb0006Len-- + zb0006Mask |= 0x8000000 } if (*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x8000000 + zb0006Len-- + zb0006Mask |= 0x10000000 } if (*z).Header.Fee.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x10000000 + zb0006Len-- + zb0006Mask |= 0x20000000 } if (*z).Header.FirstValid.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x20000000 + zb0006Len-- + zb0006Mask |= 0x40000000 } if (*z).Header.GenesisID == "" { - zb0005Len-- - zb0005Mask |= 0x40000000 + zb0006Len-- + zb0006Mask |= 0x80000000 } if (*z).Header.GenesisHash.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x80000000 + zb0006Len-- + zb0006Mask |= 0x100000000 } if (*z).Header.Group.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x100000000 + zb0006Len-- + zb0006Mask |= 0x200000000 } if (*z).Header.LastValid.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x200000000 + zb0006Len-- + zb0006Mask |= 0x400000000 } if (*z).Header.Lease == ([32]byte{}) { - zb0005Len-- - zb0005Mask |= 0x400000000 + zb0006Len-- + zb0006Mask |= 0x800000000 } if (*z).KeyregTxnFields.Nonparticipation == false { - zb0005Len-- - zb0005Mask |= 0x800000000 + zb0006Len-- + zb0006Mask |= 0x1000000000 } if len((*z).Header.Note) == 0 { - zb0005Len-- - zb0005Mask |= 0x1000000000 + zb0006Len-- + zb0006Mask |= 0x2000000000 } if (*z).PaymentTxnFields.Receiver.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x2000000000 + zb0006Len-- + zb0006Mask |= 0x4000000000 } if (*z).Header.RekeyTo.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x4000000000 + zb0006Len-- + zb0006Mask |= 0x8000000000 } if (*z).KeyregTxnFields.SelectionPK.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x8000000000 + zb0006Len-- + zb0006Mask |= 0x10000000000 } if (*z).Header.Sender.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x10000000000 + zb0006Len-- + zb0006Mask |= 0x20000000000 } if (*z).Type.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x20000000000 + zb0006Len-- + zb0006Mask |= 0x40000000000 } if (*z).KeyregTxnFields.VoteFirst.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x40000000000 + zb0006Len-- + zb0006Mask |= 0x80000000000 } if (*z).KeyregTxnFields.VoteKeyDilution == 0 { - zb0005Len-- - zb0005Mask |= 0x80000000000 + zb0006Len-- + zb0006Mask |= 0x100000000000 } if (*z).KeyregTxnFields.VotePK.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x100000000000 + zb0006Len-- + zb0006Mask |= 0x200000000000 } if (*z).KeyregTxnFields.VoteLast.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x200000000000 + zb0006Len-- + zb0006Mask |= 0x400000000000 } if (*z).AssetTransferTxnFields.XferAsset.MsgIsZero() { - zb0005Len-- - zb0005Mask |= 0x400000000000 + zb0006Len-- + zb0006Mask |= 0x800000000000 } - // variable map header, size zb0005Len - o = msgp.AppendMapHeader(o, zb0005Len) - if zb0005Len != 0 { - if (zb0005Mask & 0x100) == 0 { // if not empty + // variable map header, size zb0006Len + o = msgp.AppendMapHeader(o, zb0006Len) + if zb0006Len != 0 { + if (zb0006Mask & 0x100) == 0 { // if not empty // string "aamt" o = append(o, 0xa4, 0x61, 0x61, 0x6d, 0x74) o = msgp.AppendUint64(o, (*z).AssetTransferTxnFields.AssetAmount) } - if (zb0005Mask & 0x200) == 0 { // if not empty + if (zb0006Mask & 0x200) == 0 { // if not empty // string "aclose" o = append(o, 0xa6, 0x61, 0x63, 0x6c, 0x6f, 0x73, 0x65) o, err = (*z).AssetTransferTxnFields.AssetCloseTo.MarshalMsg(o) @@ -3756,12 +3840,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x400) == 0 { // if not empty + if (zb0006Mask & 0x400) == 0 { // if not empty // string "afrz" o = append(o, 0xa4, 0x61, 0x66, 0x72, 0x7a) o = msgp.AppendBool(o, (*z).AssetFreezeTxnFields.AssetFrozen) } - if (zb0005Mask & 0x800) == 0 { // if not empty + if (zb0006Mask & 0x800) == 0 { // if not empty // string "amt" o = append(o, 0xa3, 0x61, 0x6d, 0x74) o, err = (*z).PaymentTxnFields.Amount.MarshalMsg(o) @@ -3770,7 +3854,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x1000) == 0 { // if not empty + if (zb0006Mask & 0x1000) == 0 { // if not empty // string "apaa" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x61) if (*z).ApplicationCallTxnFields.ApplicationArgs == nil { @@ -3782,17 +3866,17 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) } } - if (zb0005Mask & 0x2000) == 0 { // if not empty + if (zb0006Mask & 0x2000) == 0 { // if not empty // string "apan" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x6e) o = msgp.AppendUint64(o, uint64((*z).ApplicationCallTxnFields.OnCompletion)) } - if (zb0005Mask & 0x4000) == 0 { // if not empty + if (zb0006Mask & 0x4000) == 0 { // if not empty // string "apap" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x70) o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ApprovalProgram) } - if (zb0005Mask & 0x8000) == 0 { // if not empty + if (zb0006Mask & 0x8000) == 0 { // if not empty // string "apar" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x72) o, err = (*z).AssetConfigTxnFields.AssetParams.MarshalMsg(o) @@ -3801,7 +3885,23 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x10000) == 0 { // if not empty + if (zb0006Mask & 0x10000) == 0 { // if not empty + // string "apas" + o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x73) + if (*z).ApplicationCallTxnFields.ForeignAssets == nil { + o = msgp.AppendNil(o) + } else { + o = msgp.AppendArrayHeader(o, uint32(len((*z).ApplicationCallTxnFields.ForeignAssets))) + } + for zb0005 := range (*z).ApplicationCallTxnFields.ForeignAssets { + o, err = (*z).ApplicationCallTxnFields.ForeignAssets[zb0005].MarshalMsg(o) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets", zb0005) + return + } + } + } + if (zb0006Mask & 0x20000) == 0 { // if not empty // string "apat" o = append(o, 0xa4, 0x61, 0x70, 0x61, 0x74) if (*z).ApplicationCallTxnFields.Accounts == nil { @@ -3817,7 +3917,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0005Mask & 0x20000) == 0 { // if not empty + if (zb0006Mask & 0x40000) == 0 { // if not empty // string "apfa" o = append(o, 0xa4, 0x61, 0x70, 0x66, 0x61) if (*z).ApplicationCallTxnFields.ForeignApps == nil { @@ -3833,7 +3933,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { } } } - if (zb0005Mask & 0x40000) == 0 { // if not empty + if (zb0006Mask & 0x80000) == 0 { // if not empty // string "apgs" o = append(o, 0xa4, 0x61, 0x70, 0x67, 0x73) o, err = (*z).ApplicationCallTxnFields.GlobalStateSchema.MarshalMsg(o) @@ -3842,7 +3942,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x80000) == 0 { // if not empty + if (zb0006Mask & 0x100000) == 0 { // if not empty // string "apid" o = append(o, 0xa4, 0x61, 0x70, 0x69, 0x64) o, err = (*z).ApplicationCallTxnFields.ApplicationID.MarshalMsg(o) @@ -3851,7 +3951,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x100000) == 0 { // if not empty + if (zb0006Mask & 0x200000) == 0 { // if not empty // string "apls" o = append(o, 0xa4, 0x61, 0x70, 0x6c, 0x73) o, err = (*z).ApplicationCallTxnFields.LocalStateSchema.MarshalMsg(o) @@ -3860,12 +3960,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x200000) == 0 { // if not empty + if (zb0006Mask & 0x400000) == 0 { // if not empty // string "apsu" o = append(o, 0xa4, 0x61, 0x70, 0x73, 0x75) o = msgp.AppendBytes(o, (*z).ApplicationCallTxnFields.ClearStateProgram) } - if (zb0005Mask & 0x400000) == 0 { // if not empty + if (zb0006Mask & 0x800000) == 0 { // if not empty // string "arcv" o = append(o, 0xa4, 0x61, 0x72, 0x63, 0x76) o, err = (*z).AssetTransferTxnFields.AssetReceiver.MarshalMsg(o) @@ -3874,7 +3974,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x800000) == 0 { // if not empty + if (zb0006Mask & 0x1000000) == 0 { // if not empty // string "asnd" o = append(o, 0xa4, 0x61, 0x73, 0x6e, 0x64) o, err = (*z).AssetTransferTxnFields.AssetSender.MarshalMsg(o) @@ -3883,7 +3983,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x1000000) == 0 { // if not empty + if (zb0006Mask & 0x2000000) == 0 { // if not empty // string "caid" o = append(o, 0xa4, 0x63, 0x61, 0x69, 0x64) o, err = (*z).AssetConfigTxnFields.ConfigAsset.MarshalMsg(o) @@ -3892,7 +3992,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x2000000) == 0 { // if not empty + if (zb0006Mask & 0x4000000) == 0 { // if not empty // string "close" o = append(o, 0xa5, 0x63, 0x6c, 0x6f, 0x73, 0x65) o, err = (*z).PaymentTxnFields.CloseRemainderTo.MarshalMsg(o) @@ -3901,7 +4001,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x4000000) == 0 { // if not empty + if (zb0006Mask & 0x8000000) == 0 { // if not empty // string "fadd" o = append(o, 0xa4, 0x66, 0x61, 0x64, 0x64) o, err = (*z).AssetFreezeTxnFields.FreezeAccount.MarshalMsg(o) @@ -3910,7 +4010,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x8000000) == 0 { // if not empty + if (zb0006Mask & 0x10000000) == 0 { // if not empty // string "faid" o = append(o, 0xa4, 0x66, 0x61, 0x69, 0x64) o, err = (*z).AssetFreezeTxnFields.FreezeAsset.MarshalMsg(o) @@ -3919,7 +4019,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x10000000) == 0 { // if not empty + if (zb0006Mask & 0x20000000) == 0 { // if not empty // string "fee" o = append(o, 0xa3, 0x66, 0x65, 0x65) o, err = (*z).Header.Fee.MarshalMsg(o) @@ -3928,7 +4028,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x20000000) == 0 { // if not empty + if (zb0006Mask & 0x40000000) == 0 { // if not empty // string "fv" o = append(o, 0xa2, 0x66, 0x76) o, err = (*z).Header.FirstValid.MarshalMsg(o) @@ -3937,12 +4037,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x40000000) == 0 { // if not empty + if (zb0006Mask & 0x80000000) == 0 { // if not empty // string "gen" o = append(o, 0xa3, 0x67, 0x65, 0x6e) o = msgp.AppendString(o, (*z).Header.GenesisID) } - if (zb0005Mask & 0x80000000) == 0 { // if not empty + if (zb0006Mask & 0x100000000) == 0 { // if not empty // string "gh" o = append(o, 0xa2, 0x67, 0x68) o, err = (*z).Header.GenesisHash.MarshalMsg(o) @@ -3951,7 +4051,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x100000000) == 0 { // if not empty + if (zb0006Mask & 0x200000000) == 0 { // if not empty // string "grp" o = append(o, 0xa3, 0x67, 0x72, 0x70) o, err = (*z).Header.Group.MarshalMsg(o) @@ -3960,7 +4060,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x200000000) == 0 { // if not empty + if (zb0006Mask & 0x400000000) == 0 { // if not empty // string "lv" o = append(o, 0xa2, 0x6c, 0x76) o, err = (*z).Header.LastValid.MarshalMsg(o) @@ -3969,22 +4069,22 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x400000000) == 0 { // if not empty + if (zb0006Mask & 0x800000000) == 0 { // if not empty // string "lx" o = append(o, 0xa2, 0x6c, 0x78) o = msgp.AppendBytes(o, ((*z).Header.Lease)[:]) } - if (zb0005Mask & 0x800000000) == 0 { // if not empty + if (zb0006Mask & 0x1000000000) == 0 { // if not empty // string "nonpart" o = append(o, 0xa7, 0x6e, 0x6f, 0x6e, 0x70, 0x61, 0x72, 0x74) o = msgp.AppendBool(o, (*z).KeyregTxnFields.Nonparticipation) } - if (zb0005Mask & 0x1000000000) == 0 { // if not empty + if (zb0006Mask & 0x2000000000) == 0 { // if not empty // string "note" o = append(o, 0xa4, 0x6e, 0x6f, 0x74, 0x65) o = msgp.AppendBytes(o, (*z).Header.Note) } - if (zb0005Mask & 0x2000000000) == 0 { // if not empty + if (zb0006Mask & 0x4000000000) == 0 { // if not empty // string "rcv" o = append(o, 0xa3, 0x72, 0x63, 0x76) o, err = (*z).PaymentTxnFields.Receiver.MarshalMsg(o) @@ -3993,7 +4093,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x4000000000) == 0 { // if not empty + if (zb0006Mask & 0x8000000000) == 0 { // if not empty // string "rekey" o = append(o, 0xa5, 0x72, 0x65, 0x6b, 0x65, 0x79) o, err = (*z).Header.RekeyTo.MarshalMsg(o) @@ -4002,7 +4102,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x8000000000) == 0 { // if not empty + if (zb0006Mask & 0x10000000000) == 0 { // if not empty // string "selkey" o = append(o, 0xa6, 0x73, 0x65, 0x6c, 0x6b, 0x65, 0x79) o, err = (*z).KeyregTxnFields.SelectionPK.MarshalMsg(o) @@ -4011,7 +4111,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x10000000000) == 0 { // if not empty + if (zb0006Mask & 0x20000000000) == 0 { // if not empty // string "snd" o = append(o, 0xa3, 0x73, 0x6e, 0x64) o, err = (*z).Header.Sender.MarshalMsg(o) @@ -4020,7 +4120,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x20000000000) == 0 { // if not empty + if (zb0006Mask & 0x40000000000) == 0 { // if not empty // string "type" o = append(o, 0xa4, 0x74, 0x79, 0x70, 0x65) o, err = (*z).Type.MarshalMsg(o) @@ -4029,7 +4129,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x40000000000) == 0 { // if not empty + if (zb0006Mask & 0x80000000000) == 0 { // if not empty // string "votefst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x66, 0x73, 0x74) o, err = (*z).KeyregTxnFields.VoteFirst.MarshalMsg(o) @@ -4038,12 +4138,12 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x80000000000) == 0 { // if not empty + if (zb0006Mask & 0x100000000000) == 0 { // if not empty // string "votekd" o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x64) o = msgp.AppendUint64(o, (*z).KeyregTxnFields.VoteKeyDilution) } - if (zb0005Mask & 0x100000000000) == 0 { // if not empty + if (zb0006Mask & 0x200000000000) == 0 { // if not empty // string "votekey" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x65, 0x79) o, err = (*z).KeyregTxnFields.VotePK.MarshalMsg(o) @@ -4052,7 +4152,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x200000000000) == 0 { // if not empty + if (zb0006Mask & 0x400000000000) == 0 { // if not empty // string "votelst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6c, 0x73, 0x74) o, err = (*z).KeyregTxnFields.VoteLast.MarshalMsg(o) @@ -4061,7 +4161,7 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte, err error) { return } } - if (zb0005Mask & 0x400000000000) == 0 { // if not empty + if (zb0006Mask & 0x800000000000) == 0 { // if not empty // string "xaid" o = append(o, 0xa4, 0x78, 0x61, 0x69, 0x64) o, err = (*z).AssetTransferTxnFields.XferAsset.MarshalMsg(o) @@ -4083,65 +4183,65 @@ func (_ *Transaction) CanMarshalMsg(z interface{}) bool { func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { var field []byte _ = field - var zb0005 int - var zb0006 bool - zb0005, zb0006, bts, err = msgp.ReadMapHeaderBytes(bts) + var zb0006 int + var zb0007 bool + zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts) if _, ok := err.(msgp.TypeError); ok { - zb0005, zb0006, bts, err = msgp.ReadArrayHeaderBytes(bts) + zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err) return } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Type.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Type") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.Sender.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Sender") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.Fee.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Fee") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.FirstValid.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FirstValid") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.LastValid.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LastValid") return } } - if zb0005 > 0 { - zb0005-- - var zb0007 int - zb0007, err = msgp.ReadBytesBytesHeader(bts) + if zb0006 > 0 { + zb0006-- + var zb0008 int + zb0008, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Note") return } - if zb0007 > config.MaxTxnNoteBytes { - err = msgp.ErrOverflow(uint64(zb0007), uint64(config.MaxTxnNoteBytes)) + if zb0008 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0008), uint64(config.MaxTxnNoteBytes)) return } (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) @@ -4150,238 +4250,238 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- (*z).Header.GenesisID, bts, err = msgp.ReadStringBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GenesisID") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.GenesisHash.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GenesisHash") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.Group.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Group") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = msgp.ReadExactBytes(bts, ((*z).Header.Lease)[:]) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Lease") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).Header.RekeyTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "RekeyTo") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).KeyregTxnFields.VotePK.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VotePK") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).KeyregTxnFields.SelectionPK.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "SelectionPK") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).KeyregTxnFields.VoteFirst.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteFirst") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).KeyregTxnFields.VoteLast.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteLast") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- (*z).KeyregTxnFields.VoteKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "VoteKeyDilution") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- (*z).KeyregTxnFields.Nonparticipation, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Nonparticipation") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).PaymentTxnFields.Receiver.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Receiver") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).PaymentTxnFields.Amount.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Amount") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).PaymentTxnFields.CloseRemainderTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "CloseRemainderTo") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetConfigTxnFields.ConfigAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ConfigAsset") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetConfigTxnFields.AssetParams.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetParams") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetTransferTxnFields.XferAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "XferAsset") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- (*z).AssetTransferTxnFields.AssetAmount, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetAmount") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetTransferTxnFields.AssetSender.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetSender") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetTransferTxnFields.AssetReceiver.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetReceiver") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetTransferTxnFields.AssetCloseTo.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetCloseTo") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetFreezeTxnFields.FreezeAccount.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FreezeAccount") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).AssetFreezeTxnFields.FreezeAsset.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "FreezeAsset") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- (*z).AssetFreezeTxnFields.AssetFrozen, bts, err = msgp.ReadBoolBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "AssetFrozen") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).ApplicationCallTxnFields.ApplicationID.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApplicationID") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- { - var zb0008 uint64 - zb0008, bts, err = msgp.ReadUint64Bytes(bts) + var zb0009 uint64 + zb0009, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "OnCompletion") return } - (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0008) + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0009) } } - if zb0005 > 0 { - zb0005-- - var zb0009 int - var zb0010 bool - zb0009, zb0010, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0006 > 0 { + zb0006-- + var zb0010 int + var zb0011 bool + zb0010, zb0011, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0009 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0009), uint64(encodedMaxApplicationArgs)) + if zb0010 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0010), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "struct-from-array", "ApplicationArgs") return } - if zb0010 { + if zb0011 { (*z).ApplicationCallTxnFields.ApplicationArgs = nil - } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0009 { - (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0009] + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0010 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0010] } else { - (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0009) + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0010) } for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) @@ -4391,26 +4491,26 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - if zb0005 > 0 { - zb0005-- - var zb0011 int - var zb0012 bool - zb0011, zb0012, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0006 > 0 { + zb0006-- + var zb0012 int + var zb0013 bool + zb0012, zb0013, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0011 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0011), uint64(encodedMaxAccounts)) + if zb0012 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0012), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "struct-from-array", "Accounts") return } - if zb0012 { + if zb0013 { (*z).ApplicationCallTxnFields.Accounts = nil - } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0011 { - (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0011] + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0012 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0012] } else { - (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0011) + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0012) } for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) @@ -4420,26 +4520,26 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - if zb0005 > 0 { - zb0005-- - var zb0013 int - var zb0014 bool - zb0013, zb0014, bts, err = msgp.ReadArrayHeaderBytes(bts) + if zb0006 > 0 { + zb0006-- + var zb0014 int + var zb0015 bool + zb0014, zb0015, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0013 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0013), uint64(encodedMaxForeignApps)) + if zb0014 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0014), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "struct-from-array", "ForeignApps") return } - if zb0014 { + if zb0015 { (*z).ApplicationCallTxnFields.ForeignApps = nil - } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0013 { - (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0013] + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0014 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0014] } else { - (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0013) + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0014) } for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) @@ -4449,32 +4549,61 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- + var zb0016 int + var zb0017 bool + zb0016, zb0017, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets") + return + } + if zb0016 > encodedMaxForeignAssets { + err = msgp.ErrOverflow(uint64(zb0016), uint64(encodedMaxForeignAssets)) + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets") + return + } + if zb0017 { + (*z).ApplicationCallTxnFields.ForeignAssets = nil + } else if (*z).ApplicationCallTxnFields.ForeignAssets != nil && cap((*z).ApplicationCallTxnFields.ForeignAssets) >= zb0016 { + (*z).ApplicationCallTxnFields.ForeignAssets = ((*z).ApplicationCallTxnFields.ForeignAssets)[:zb0016] + } else { + (*z).ApplicationCallTxnFields.ForeignAssets = make([]basics.AssetIndex, zb0016) + } + for zb0005 := range (*z).ApplicationCallTxnFields.ForeignAssets { + bts, err = (*z).ApplicationCallTxnFields.ForeignAssets[zb0005].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "ForeignAssets", zb0005) + return + } + } + } + if zb0006 > 0 { + zb0006-- bts, err = (*z).ApplicationCallTxnFields.LocalStateSchema.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "LocalStateSchema") return } } - if zb0005 > 0 { - zb0005-- + if zb0006 > 0 { + zb0006-- bts, err = (*z).ApplicationCallTxnFields.GlobalStateSchema.UnmarshalMsg(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "GlobalStateSchema") return } } - if zb0005 > 0 { - zb0005-- - var zb0015 int - zb0015, err = msgp.ReadBytesBytesHeader(bts) + if zb0006 > 0 { + zb0006-- + var zb0018 int + zb0018, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ApprovalProgram") return } - if zb0015 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0015), uint64(config.MaxAppProgramLen)) + if zb0018 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0018), uint64(config.MaxAppProgramLen)) return } (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) @@ -4483,16 +4612,16 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } - if zb0005 > 0 { - zb0005-- - var zb0016 int - zb0016, err = msgp.ReadBytesBytesHeader(bts) + if zb0006 > 0 { + zb0006-- + var zb0019 int + zb0019, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "struct-from-array", "ClearStateProgram") return } - if zb0016 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0016), uint64(config.MaxAppProgramLen)) + if zb0019 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0019), uint64(config.MaxAppProgramLen)) return } (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) @@ -4501,8 +4630,8 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } - if zb0005 > 0 { - err = msgp.ErrTooManyArrayFields(zb0005) + if zb0006 > 0 { + err = msgp.ErrTooManyArrayFields(zb0006) if err != nil { err = msgp.WrapError(err, "struct-from-array") return @@ -4513,11 +4642,11 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { err = msgp.WrapError(err) return } - if zb0006 { + if zb0007 { (*z) = Transaction{} } - for zb0005 > 0 { - zb0005-- + for zb0006 > 0 { + zb0006-- field, bts, err = msgp.ReadMapKeyZC(bts) if err != nil { err = msgp.WrapError(err) @@ -4555,14 +4684,14 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "note": - var zb0017 int - zb0017, err = msgp.ReadBytesBytesHeader(bts) + var zb0020 int + zb0020, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "Note") return } - if zb0017 > config.MaxTxnNoteBytes { - err = msgp.ErrOverflow(uint64(zb0017), uint64(config.MaxTxnNoteBytes)) + if zb0020 > config.MaxTxnNoteBytes { + err = msgp.ErrOverflow(uint64(zb0020), uint64(config.MaxTxnNoteBytes)) return } (*z).Header.Note, bts, err = msgp.ReadBytesBytes(bts, (*z).Header.Note) @@ -4722,33 +4851,33 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } case "apan": { - var zb0018 uint64 - zb0018, bts, err = msgp.ReadUint64Bytes(bts) + var zb0021 uint64 + zb0021, bts, err = msgp.ReadUint64Bytes(bts) if err != nil { err = msgp.WrapError(err, "OnCompletion") return } - (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0018) + (*z).ApplicationCallTxnFields.OnCompletion = OnCompletion(zb0021) } case "apaa": - var zb0019 int - var zb0020 bool - zb0019, zb0020, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0022 int + var zb0023 bool + zb0022, zb0023, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0019 > encodedMaxApplicationArgs { - err = msgp.ErrOverflow(uint64(zb0019), uint64(encodedMaxApplicationArgs)) + if zb0022 > encodedMaxApplicationArgs { + err = msgp.ErrOverflow(uint64(zb0022), uint64(encodedMaxApplicationArgs)) err = msgp.WrapError(err, "ApplicationArgs") return } - if zb0020 { + if zb0023 { (*z).ApplicationCallTxnFields.ApplicationArgs = nil - } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0019 { - (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0019] + } else if (*z).ApplicationCallTxnFields.ApplicationArgs != nil && cap((*z).ApplicationCallTxnFields.ApplicationArgs) >= zb0022 { + (*z).ApplicationCallTxnFields.ApplicationArgs = ((*z).ApplicationCallTxnFields.ApplicationArgs)[:zb0022] } else { - (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0019) + (*z).ApplicationCallTxnFields.ApplicationArgs = make([][]byte, zb0022) } for zb0002 := range (*z).ApplicationCallTxnFields.ApplicationArgs { (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002], bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApplicationArgs[zb0002]) @@ -4758,24 +4887,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apat": - var zb0021 int - var zb0022 bool - zb0021, zb0022, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0024 int + var zb0025 bool + zb0024, zb0025, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "Accounts") return } - if zb0021 > encodedMaxAccounts { - err = msgp.ErrOverflow(uint64(zb0021), uint64(encodedMaxAccounts)) + if zb0024 > encodedMaxAccounts { + err = msgp.ErrOverflow(uint64(zb0024), uint64(encodedMaxAccounts)) err = msgp.WrapError(err, "Accounts") return } - if zb0022 { + if zb0025 { (*z).ApplicationCallTxnFields.Accounts = nil - } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0021 { - (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0021] + } else if (*z).ApplicationCallTxnFields.Accounts != nil && cap((*z).ApplicationCallTxnFields.Accounts) >= zb0024 { + (*z).ApplicationCallTxnFields.Accounts = ((*z).ApplicationCallTxnFields.Accounts)[:zb0024] } else { - (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0021) + (*z).ApplicationCallTxnFields.Accounts = make([]basics.Address, zb0024) } for zb0003 := range (*z).ApplicationCallTxnFields.Accounts { bts, err = (*z).ApplicationCallTxnFields.Accounts[zb0003].UnmarshalMsg(bts) @@ -4785,24 +4914,24 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { } } case "apfa": - var zb0023 int - var zb0024 bool - zb0023, zb0024, bts, err = msgp.ReadArrayHeaderBytes(bts) + var zb0026 int + var zb0027 bool + zb0026, zb0027, bts, err = msgp.ReadArrayHeaderBytes(bts) if err != nil { err = msgp.WrapError(err, "ForeignApps") return } - if zb0023 > encodedMaxForeignApps { - err = msgp.ErrOverflow(uint64(zb0023), uint64(encodedMaxForeignApps)) + if zb0026 > encodedMaxForeignApps { + err = msgp.ErrOverflow(uint64(zb0026), uint64(encodedMaxForeignApps)) err = msgp.WrapError(err, "ForeignApps") return } - if zb0024 { + if zb0027 { (*z).ApplicationCallTxnFields.ForeignApps = nil - } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0023 { - (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0023] + } else if (*z).ApplicationCallTxnFields.ForeignApps != nil && cap((*z).ApplicationCallTxnFields.ForeignApps) >= zb0026 { + (*z).ApplicationCallTxnFields.ForeignApps = ((*z).ApplicationCallTxnFields.ForeignApps)[:zb0026] } else { - (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0023) + (*z).ApplicationCallTxnFields.ForeignApps = make([]basics.AppIndex, zb0026) } for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { bts, err = (*z).ApplicationCallTxnFields.ForeignApps[zb0004].UnmarshalMsg(bts) @@ -4811,6 +4940,33 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } } + case "apas": + var zb0028 int + var zb0029 bool + zb0028, zb0029, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets") + return + } + if zb0028 > encodedMaxForeignAssets { + err = msgp.ErrOverflow(uint64(zb0028), uint64(encodedMaxForeignAssets)) + err = msgp.WrapError(err, "ForeignAssets") + return + } + if zb0029 { + (*z).ApplicationCallTxnFields.ForeignAssets = nil + } else if (*z).ApplicationCallTxnFields.ForeignAssets != nil && cap((*z).ApplicationCallTxnFields.ForeignAssets) >= zb0028 { + (*z).ApplicationCallTxnFields.ForeignAssets = ((*z).ApplicationCallTxnFields.ForeignAssets)[:zb0028] + } else { + (*z).ApplicationCallTxnFields.ForeignAssets = make([]basics.AssetIndex, zb0028) + } + for zb0005 := range (*z).ApplicationCallTxnFields.ForeignAssets { + bts, err = (*z).ApplicationCallTxnFields.ForeignAssets[zb0005].UnmarshalMsg(bts) + if err != nil { + err = msgp.WrapError(err, "ForeignAssets", zb0005) + return + } + } case "apls": bts, err = (*z).ApplicationCallTxnFields.LocalStateSchema.UnmarshalMsg(bts) if err != nil { @@ -4824,14 +4980,14 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "apap": - var zb0025 int - zb0025, err = msgp.ReadBytesBytesHeader(bts) + var zb0030 int + zb0030, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "ApprovalProgram") return } - if zb0025 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0025), uint64(config.MaxAppProgramLen)) + if zb0030 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0030), uint64(config.MaxAppProgramLen)) return } (*z).ApplicationCallTxnFields.ApprovalProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ApprovalProgram) @@ -4840,14 +4996,14 @@ func (z *Transaction) UnmarshalMsg(bts []byte) (o []byte, err error) { return } case "apsu": - var zb0026 int - zb0026, err = msgp.ReadBytesBytesHeader(bts) + var zb0031 int + zb0031, err = msgp.ReadBytesBytesHeader(bts) if err != nil { err = msgp.WrapError(err, "ClearStateProgram") return } - if zb0026 > config.MaxAppProgramLen { - err = msgp.ErrOverflow(uint64(zb0026), uint64(config.MaxAppProgramLen)) + if zb0031 > config.MaxAppProgramLen { + err = msgp.ErrOverflow(uint64(zb0031), uint64(config.MaxAppProgramLen)) return } (*z).ApplicationCallTxnFields.ClearStateProgram, bts, err = msgp.ReadBytesBytes(bts, (*z).ApplicationCallTxnFields.ClearStateProgram) @@ -4887,13 +5043,17 @@ func (z *Transaction) Msgsize() (s int) { for zb0004 := range (*z).ApplicationCallTxnFields.ForeignApps { s += (*z).ApplicationCallTxnFields.ForeignApps[zb0004].Msgsize() } + s += 5 + msgp.ArrayHeaderSize + for zb0005 := range (*z).ApplicationCallTxnFields.ForeignAssets { + s += (*z).ApplicationCallTxnFields.ForeignAssets[zb0005].Msgsize() + } s += 5 + (*z).ApplicationCallTxnFields.LocalStateSchema.Msgsize() + 5 + (*z).ApplicationCallTxnFields.GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ClearStateProgram) return } // MsgIsZero returns whether this is a zero value func (z *Transaction) MsgIsZero() bool { - return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) && ((*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero()) && ((*z).ApplicationCallTxnFields.OnCompletion == 0) && (len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0) && (len((*z).ApplicationCallTxnFields.Accounts) == 0) && (len((*z).ApplicationCallTxnFields.ForeignApps) == 0) && ((*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero()) && ((*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero()) && (len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0) && (len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0) + return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) && ((*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero()) && ((*z).ApplicationCallTxnFields.OnCompletion == 0) && (len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0) && (len((*z).ApplicationCallTxnFields.Accounts) == 0) && (len((*z).ApplicationCallTxnFields.ForeignApps) == 0) && (len((*z).ApplicationCallTxnFields.ForeignAssets) == 0) && ((*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero()) && ((*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero()) && (len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0) && (len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0) } // MarshalMsg implements msgp.Marshaler diff --git a/ledger/applications.go b/ledger/applications.go index 6ee9e67aa7..44a326298b 100644 --- a/ledger/applications.go +++ b/ledger/applications.go @@ -200,9 +200,20 @@ func (al *appLedger) AssetHolding(addr basics.Address, assetIdx basics.AssetInde return holding, nil } -func (al *appLedger) AssetParams(addr basics.Address, assetIdx basics.AssetIndex) (basics.AssetParams, error) { +func (al *appLedger) AssetParams(assetIdx basics.AssetIndex) (basics.AssetParams, error) { + // Find asset creator + creator, ok, err := al.balances.GetCreator(basics.CreatableIndex(assetIdx), basics.AssetCreatable) + if err != nil { + return basics.AssetParams{}, err + } + + // Ensure asset exists + if !ok { + return basics.AssetParams{}, fmt.Errorf("asset %d does not exist", assetIdx) + } + // Fetch the requested balance record - record, err := al.balances.Get(addr, false) + record, err := al.balances.Get(creator, false) if err != nil { return basics.AssetParams{}, err } @@ -210,7 +221,7 @@ func (al *appLedger) AssetParams(addr basics.Address, assetIdx basics.AssetIndex // Ensure account created the requested asset params, ok := record.AssetParams[assetIdx] if !ok { - err = fmt.Errorf("account %s has not created asset %d", addr.String(), assetIdx) + err = fmt.Errorf("account %s has not created asset %d", creator, assetIdx) return basics.AssetParams{}, err } diff --git a/ledger/applications_test.go b/ledger/applications_test.go index 0ba423b7f0..e75fe80393 100644 --- a/ledger/applications_test.go +++ b/ledger/applications_test.go @@ -183,14 +183,14 @@ func TestAppLedgerAsset(t *testing.T) { assetIdx := basics.AssetIndex(2) b.balances = map[basics.Address]basics.AccountData{addr1: {}} - _, err = l.AssetParams(addr1, assetIdx) + _, err = l.AssetParams(assetIdx) a.Error(err) a.Contains(err.Error(), "has not created asset") b.balances[addr1] = basics.AccountData{ AssetParams: map[basics.AssetIndex]basics.AssetParams{assetIdx: {Total: 1000}}, } - ap, err := l.AssetParams(addr1, assetIdx) + ap, err := l.AssetParams(assetIdx) a.NoError(err) a.Equal(uint64(1000), ap.Total) delete(b.balances, addr1) From 455496f457947871e9098e5f8aeb242919ff7cdc Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 29 Jul 2020 12:52:39 -0400 Subject: [PATCH 197/267] Add missing application index. --- daemon/algod/api/algod.oas2.json | 4 + daemon/algod/api/algod.oas3.yml | 8 + .../api/server/v2/generated/private/routes.go | 176 +++++----- .../api/server/v2/generated/private/types.go | 3 + .../algod/api/server/v2/generated/routes.go | 312 +++++++++--------- daemon/algod/api/server/v2/generated/types.go | 3 + daemon/algod/api/server/v2/handlers.go | 2 + daemon/algod/api/server/v2/utils.go | 39 ++- 8 files changed, 301 insertions(+), 246 deletions(-) diff --git a/daemon/algod/api/algod.oas2.json b/daemon/algod/api/algod.oas2.json index 05d513e22e..bf7b88a1c3 100644 --- a/daemon/algod/api/algod.oas2.json +++ b/daemon/algod/api/algod.oas2.json @@ -2073,6 +2073,10 @@ "description": "The asset index if the transaction was found and it created an asset.", "type": "integer" }, + "application-index": { + "description": "The application index if the transaction was found and it created an application.", + "type": "integer" + }, "close-rewards": { "description": "Rewards in microalgos applied to the close remainder to account.", "type": "integer" diff --git a/daemon/algod/api/algod.oas3.yml b/daemon/algod/api/algod.oas3.yml index 3f2f700a54..2c3c98390e 100644 --- a/daemon/algod/api/algod.oas3.yml +++ b/daemon/algod/api/algod.oas3.yml @@ -459,6 +459,10 @@ "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" @@ -3010,6 +3014,10 @@ "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" diff --git a/daemon/algod/api/server/v2/generated/private/routes.go b/daemon/algod/api/server/v2/generated/private/routes.go index 20b205d497..5cfcc95a5a 100644 --- a/daemon/algod/api/server/v2/generated/private/routes.go +++ b/daemon/algod/api/server/v2/generated/private/routes.go @@ -272,94 +272,94 @@ var swaggerSpec = []string{ "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", - "TGHjh4aoHz0LeOLefLj5vwEAAP//HPdY54CEAAA=", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/private/types.go b/daemon/algod/api/server/v2/generated/private/types.go index bde0d54ebd..bb3626a7ae 100644 --- a/daemon/algod/api/server/v2/generated/private/types.go +++ b/daemon/algod/api/server/v2/generated/private/types.go @@ -525,6 +525,9 @@ type NodeStatusResponse struct { // PendingTransactionResponse defines model for PendingTransactionResponse. type PendingTransactionResponse struct { + // The application index if the transaction was found and it created an application. + ApplicationIndex *uint64 `json:"application-index,omitempty"` + // The asset index if the transaction was found and it created an asset. AssetIndex *uint64 `json:"asset-index,omitempty"` diff --git a/daemon/algod/api/server/v2/generated/routes.go b/daemon/algod/api/server/v2/generated/routes.go index 2c061ccbd5..b131d52da0 100644 --- a/daemon/algod/api/server/v2/generated/routes.go +++ b/daemon/algod/api/server/v2/generated/routes.go @@ -561,162 +561,162 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "yBp12QAA", + "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", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/daemon/algod/api/server/v2/generated/types.go b/daemon/algod/api/server/v2/generated/types.go index db5017e151..c3b918387f 100644 --- a/daemon/algod/api/server/v2/generated/types.go +++ b/daemon/algod/api/server/v2/generated/types.go @@ -525,6 +525,9 @@ type NodeStatusResponse struct { // PendingTransactionResponse defines model for PendingTransactionResponse. type PendingTransactionResponse struct { + // The application index if the transaction was found and it created an application. + ApplicationIndex *uint64 `json:"application-index,omitempty"` + // The asset index if the transaction was found and it created an asset. AssetIndex *uint64 `json:"asset-index,omitempty"` diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index e21774e7c4..d7c612f743 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -443,6 +443,7 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, // Encoding wasn't working well without embedding "real" objects. response := struct { AssetIndex *uint64 `codec:"asset-index,omitempty"` + ApplicationIndex *uint64 `codec:"application-index,omitempty"` CloseRewards *uint64 `codec:"close-rewards,omitempty"` ClosingAmount *uint64 `codec:"closing-amount,omitempty"` ConfirmedRound *uint64 `codec:"confirmed-round,omitempty"` @@ -471,6 +472,7 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response.CloseRewards = &txn.ApplyData.CloseRewards.Raw response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.Ledger()) + response.ApplicationIndex = computeApplicationIndexFromTxn(txn, v2.Node.Ledger()) response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn) } diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index 316bc0995c..9a7994b360 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -85,7 +85,7 @@ func byteOrNil(data []byte) *[]byte { return &data } -func computeAssetIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx *uint64) { +func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx *uint64) { // Compute transaction index in block offset := -1 for idx, stxnib := range payset { @@ -137,7 +137,42 @@ func computeAssetIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx *uint return nil } - return computeAssetIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) + return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) +} + +// computeAssetIndexFromTxn returns the created asset index given a confirmed +// transaction whose confirmation block is available in the ledger. Note that +// 0 is an invalid asset index (they start at 1). +func computeApplicationIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx *uint64) { + // Must have ledger + if l == nil { + return nil + } + // Transaction must be confirmed + if tx.ConfirmedRound == 0 { + return nil + } + // Transaction must be AssetConfig transaction + if tx.Txn.Txn.ApplicationID == 0 { + return nil + } + // Transaction must be creating an asset + if tx.Txn.Txn.AssetConfigTxnFields.ConfigAsset != 0 { + return nil + } + + // Look up block where transaction was confirmed + blk, err := l.Block(tx.ConfirmedRound) + if err != nil { + return nil + } + + payset, err := blk.DecodePaysetFlat() + if err != nil { + return nil + } + + return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) } // getCodecHandle converts a format string into the encoder + content type From 43a93c37ebd9da113e39fdb1931408fb0983e2e4 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 13:07:58 -0400 Subject: [PATCH 198/267] add ForeignAssets support to goal, fix e2e test --- cmd/goal/application.go | 67 +++++++++++-------- cmd/goal/interact.go | 3 +- config/consensus.go | 2 +- libgoal/transactions.go | 38 ++++++----- shared/pingpong/accounts.go | 2 +- shared/pingpong/pingpong.go | 2 +- .../features/transactions/accountv2_test.go | 4 +- .../e2e_subs/e2e-app-real-assets-round.sh | 6 +- .../e2e_subs/tealprogs/assetround.teal | 16 ++--- 9 files changed, 77 insertions(+), 63 deletions(-) diff --git a/cmd/goal/application.go b/cmd/goal/application.go index c0ca7dc776..1a4be147c7 100644 --- a/cmd/goal/application.go +++ b/cmd/goal/application.go @@ -58,6 +58,7 @@ var ( // uint64s from strings for now. 4bn transactions and using a 32-bit // platform seems not so far-fetched? foreignApps []string + foreignAssets []string appStrAccounts []string appArgs []string @@ -82,6 +83,7 @@ func init() { appCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") appCmd.PersistentFlags().StringSliceVar(&appArgs, "app-arg", nil, "Args to encode for application transactions (all will be encoded to a byte slice). For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.") appCmd.PersistentFlags().StringSliceVar(&foreignApps, "foreign-app", nil, "Indexes of other apps whose global state is read in this transaction") + appCmd.PersistentFlags().StringSliceVar(&foreignAssets, "foreign-asset", nil, "Indexes of assets whose parameters are read in this transaction") appCmd.PersistentFlags().StringSliceVar(&appStrAccounts, "app-account", nil, "Accounts that may be accessed from application logic") appCmd.PersistentFlags().StringVarP(&appInputFilename, "app-input", "i", "", "JSON file containing encoded arguments and inputs (mutually exclusive with app-arg-b64 and app-account)") @@ -166,15 +168,16 @@ type appCallArg struct { } type appCallInputs struct { - Accounts []string `codec:"accounts"` - ForeignApps []uint64 `codec:"foreignapps"` - Args []appCallArg `codec:"args"` + Accounts []string `codec:"accounts"` + ForeignApps []uint64 `codec:"foreignapps"` + ForeignAssets []uint64 `codec:"foreignassets"` + Args []appCallArg `codec:"args"` } -func getForeignApps() []uint64 { - out := make([]uint64, len(foreignApps)) - for i, app := range foreignApps { - parsed, err := strconv.ParseUint(app, 10, 64) +func stringsToUint64(strs []string) []uint64 { + out := make([]uint64, len(strs)) + for i, idstr := range strs { + parsed, err := strconv.ParseUint(idstr, 10, 64) if err != nil { reportErrorf("Could not parse foreign app id: %v", err) } @@ -183,6 +186,14 @@ func getForeignApps() []uint64 { return out } +func getForeignAssets() []uint64 { + return stringsToUint64(foreignAssets) +} + +func getForeignApps() []uint64 { + return stringsToUint64(foreignApps) +} + func parseAppArg(arg appCallArg) (rawValue []byte, parseErr error) { switch arg.Encoding { case "str", "string": @@ -223,9 +234,10 @@ func parseAppArg(arg appCallArg) (rawValue []byte, parseErr error) { return } -func parseAppInputs(inputs appCallInputs) (args [][]byte, accounts []string, foreignApps []uint64) { +func parseAppInputs(inputs appCallInputs) (args [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) { accounts = inputs.Accounts foreignApps = inputs.ForeignApps + foreignAssets = inputs.ForeignAssets args = make([][]byte, len(inputs.Args)) for i, arg := range inputs.Args { rawValue, err := parseAppArg(arg) @@ -237,7 +249,7 @@ func parseAppInputs(inputs appCallInputs) (args [][]byte, accounts []string, for return } -func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint64) { +func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) { var inputs appCallInputs f, err := os.Open(appInputFilename) if err != nil { @@ -253,7 +265,7 @@ func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint return parseAppInputs(inputs) } -func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64) { +func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) { if (appArgs != nil || appStrAccounts != nil || foreignApps != nil) && appInputFilename != "" { reportErrorf("Cannot specify both command-line arguments/accounts and JSON input filename") } @@ -275,9 +287,10 @@ func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64) { } inputs := appCallInputs{ - Accounts: appStrAccounts, - ForeignApps: getForeignApps(), - Args: encodedArgs, + Accounts: appStrAccounts, + ForeignApps: getForeignApps(), + ForeignAssets: getForeignAssets(), + Args: encodedArgs, } return parseAppInputs(inputs) @@ -360,14 +373,14 @@ var createAppCmd = &cobra.Command{ // Parse transaction parameters approvalProg, clearProg := mustParseProgArgs() onCompletion := mustParseOnCompletion(createOnCompletion) - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() switch onCompletion { case transactions.CloseOutOC, transactions.ClearStateOC: reportWarnf("'--on-completion %s' may be ill-formed for 'goal app create'", createOnCompletion) } - tx, err := client.MakeUnsignedAppCreateTx(onCompletion, approvalProg, clearProg, globalSchema, localSchema, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppCreateTx(onCompletion, approvalProg, clearProg, globalSchema, localSchema, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -448,9 +461,9 @@ var updateAppCmd = &cobra.Command{ // Parse transaction parameters approvalProg, clearProg := mustParseProgArgs() - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppUpdateTx(appIdx, appArgs, appAccounts, foreignApps, approvalProg, clearProg) + tx, err := client.MakeUnsignedAppUpdateTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets, approvalProg, clearProg) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -526,9 +539,9 @@ var optInAppCmd = &cobra.Command{ client := ensureFullClient(dataDir) // Parse transaction parameters - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppOptInTx(appIdx, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppOptInTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -604,9 +617,9 @@ var closeOutAppCmd = &cobra.Command{ client := ensureFullClient(dataDir) // Parse transaction parameters - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppCloseOutTx(appIdx, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppCloseOutTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -682,9 +695,9 @@ var clearAppCmd = &cobra.Command{ client := ensureFullClient(dataDir) // Parse transaction parameters - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppClearStateTx(appIdx, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppClearStateTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -760,9 +773,9 @@ var callAppCmd = &cobra.Command{ client := ensureFullClient(dataDir) // Parse transaction parameters - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppNoOpTx(appIdx, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppNoOpTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } @@ -838,9 +851,9 @@ var deleteAppCmd = &cobra.Command{ client := ensureFullClient(dataDir) // Parse transaction parameters - appArgs, appAccounts, foreignApps := getAppInputs() + appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() - tx, err := client.MakeUnsignedAppDeleteTx(appIdx, appArgs, appAccounts, foreignApps) + tx, err := client.MakeUnsignedAppDeleteTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets) if err != nil { reportErrorf("Cannot create application txn: %v", err) } diff --git a/cmd/goal/interact.go b/cmd/goal/interact.go index 6b4785b338..fc4e709827 100644 --- a/cmd/goal/interact.go +++ b/cmd/goal/interact.go @@ -557,6 +557,7 @@ var appExecuteCmd = &cobra.Command{ onCompletion := mustParseOnCompletion(proc.OnCompletion) appAccounts := inputs.Accounts foreignApps := inputs.ForeignApps + foreignAssets := inputs.ForeignAssets appArgs := make([][]byte, len(inputs.Args)) for i, arg := range inputs.Args { @@ -581,7 +582,7 @@ var appExecuteCmd = &cobra.Command{ localSchema = header.Query.Local.ToStateSchema() globalSchema = header.Query.Global.ToStateSchema() } - tx, err := client.MakeUnsignedApplicationCallTx(appIdx, appArgs, appAccounts, foreignApps, onCompletion, approvalProg, clearProg, globalSchema, localSchema) + tx, err := client.MakeUnsignedApplicationCallTx(appIdx, appArgs, appAccounts, foreignApps, foreignAssets, onCompletion, approvalProg, clearProg, globalSchema, localSchema) if err != nil { reportErrorf("Cannot create application txn: %v", err) } diff --git a/config/consensus.go b/config/consensus.go index 467f904238..14148c486e 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -500,7 +500,7 @@ func initConsensusProtocols() { MaxBalLookback: 320, - MaxTxGroupSize: 1, + MaxTxGroupSize: 1, } v7.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} diff --git a/libgoal/transactions.go b/libgoal/transactions.go index 514ce5d5e8..ec0ce5d8db 100644 --- a/libgoal/transactions.go +++ b/libgoal/transactions.go @@ -387,50 +387,50 @@ func (c *Client) FillUnsignedTxTemplate(sender string, firstValid, lastValid, fe } // MakeUnsignedAppCreateTx makes a transaction for creating an application -func (c *Client) MakeUnsignedAppCreateTx(onComplete transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(0, appArgs, accounts, foreignApps, onComplete, approvalProg, clearProg, globalSchema, localSchema) +func (c *Client) MakeUnsignedAppCreateTx(onComplete transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(0, appArgs, accounts, foreignApps, foreignAssets, onComplete, approvalProg, clearProg, globalSchema, localSchema) } // MakeUnsignedAppUpdateTx makes a transaction for updating an application's programs -func (c *Client) MakeUnsignedAppUpdateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, approvalProg []byte, clearProg []byte) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.UpdateApplicationOC, approvalProg, clearProg, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppUpdateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64, approvalProg []byte, clearProg []byte) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.UpdateApplicationOC, approvalProg, clearProg, emptySchema, emptySchema) } // MakeUnsignedAppDeleteTx makes a transaction for deleting an application -func (c *Client) MakeUnsignedAppDeleteTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.DeleteApplicationOC, nil, nil, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppDeleteTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.DeleteApplicationOC, nil, nil, emptySchema, emptySchema) } // MakeUnsignedAppOptInTx makes a transaction for opting in to (allocating // some account-specific state for) an application -func (c *Client) MakeUnsignedAppOptInTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.OptInOC, nil, nil, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppOptInTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.OptInOC, nil, nil, emptySchema, emptySchema) } // MakeUnsignedAppCloseOutTx makes a transaction for closing out of // (deallocating all account-specific state for) an application -func (c *Client) MakeUnsignedAppCloseOutTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.CloseOutOC, nil, nil, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppCloseOutTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.CloseOutOC, nil, nil, emptySchema, emptySchema) } // MakeUnsignedAppClearStateTx makes a transaction for clearing out all // account-specific state for an application. It may not be rejected by the // application's logic. -func (c *Client) MakeUnsignedAppClearStateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.ClearStateOC, nil, nil, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppClearStateTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.ClearStateOC, nil, nil, emptySchema, emptySchema) } // MakeUnsignedAppNoOpTx makes a transaction for interacting with an existing // application, potentially updating any account-specific local state and // global state associated with it. -func (c *Client) MakeUnsignedAppNoOpTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64) (tx transactions.Transaction, err error) { - return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, transactions.NoOpOC, nil, nil, emptySchema, emptySchema) +func (c *Client) MakeUnsignedAppNoOpTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) (tx transactions.Transaction, err error) { + return c.MakeUnsignedApplicationCallTx(appIdx, appArgs, accounts, foreignApps, foreignAssets, transactions.NoOpOC, nil, nil, emptySchema, emptySchema) } // MakeUnsignedApplicationCallTx is a helper for the above ApplicationCall // transaction constructors. A fully custom ApplicationCall transaction may // be constructed using this method. -func (c *Client) MakeUnsignedApplicationCallTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, onCompletion transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema) (tx transactions.Transaction, err error) { +func (c *Client) MakeUnsignedApplicationCallTx(appIdx uint64, appArgs [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64, onCompletion transactions.OnCompletion, approvalProg []byte, clearProg []byte, globalSchema basics.StateSchema, localSchema basics.StateSchema) (tx transactions.Transaction, err error) { tx.Type = protocol.ApplicationCallTx tx.ApplicationID = basics.AppIndex(appIdx) tx.OnCompletion = onCompletion @@ -442,6 +442,7 @@ func (c *Client) MakeUnsignedApplicationCallTx(appIdx uint64, appArgs [][]byte, } tx.ForeignApps = parseTxnForeignApps(foreignApps) + tx.ForeignAssets = parseTxnForeignAssets(foreignAssets) tx.ApprovalProgram = approvalProg tx.ClearStateProgram = clearProg tx.LocalStateSchema = localSchema @@ -468,6 +469,13 @@ func parseTxnForeignApps(foreignApps []uint64) (parsed []basics.AppIndex) { return } +func parseTxnForeignAssets(foreignAssets []uint64) (parsed []basics.AssetIndex) { + for _, aidx := range foreignAssets { + parsed = append(parsed, basics.AssetIndex(aidx)) + } + return +} + // MakeUnsignedAssetCreateTx creates a tx template for creating // an asset. // diff --git a/shared/pingpong/accounts.go b/shared/pingpong/accounts.go index aa6824ba00..e83faeee29 100644 --- a/shared/pingpong/accounts.go +++ b/shared/pingpong/accounts.go @@ -449,7 +449,7 @@ func prepareApps(accounts map[string]uint64, client libgoal.Client, cfg PpConfig globSchema := basics.StateSchema{NumByteSlice: 64} locSchema := basics.StateSchema{} - tx, err = client.MakeUnsignedAppCreateTx(transactions.NoOpOC, prog, prog, globSchema, locSchema, nil, nil, nil) + tx, err = client.MakeUnsignedAppCreateTx(transactions.NoOpOC, prog, prog, globSchema, locSchema, nil, nil, nil, nil) if err != nil { fmt.Printf("Cannot create app txn\n") return diff --git a/shared/pingpong/pingpong.go b/shared/pingpong/pingpong.go index 3f9fbcec36..8818800fe2 100644 --- a/shared/pingpong/pingpong.go +++ b/shared/pingpong/pingpong.go @@ -452,7 +452,7 @@ func constructTxn(from, to string, fee, amt, aidx uint64, client libgoal.Client, } if cfg.NumApp > 0 { // Construct app transaction - txn, err = client.MakeUnsignedAppNoOpTx(aidx, nil, nil, nil) + txn, err = client.MakeUnsignedAppNoOpTx(aidx, nil, nil, nil, nil) if err != nil { return } diff --git a/test/e2e-go/features/transactions/accountv2_test.go b/test/e2e-go/features/transactions/accountv2_test.go index 695d43c3fe..347cf13fa2 100644 --- a/test/e2e-go/features/transactions/accountv2_test.go +++ b/test/e2e-go/features/transactions/accountv2_test.go @@ -112,7 +112,7 @@ int 1 // create the app tx, err := client.MakeUnsignedAppCreateTx( - transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, nil, ) a.NoError(err) tx, err = client.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) @@ -155,7 +155,7 @@ int 1 a.Equal(uint64(1), value.Uint) // call the app - tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil, nil) a.NoError(err) tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) a.NoError(err) diff --git a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh index ac6841b135..6db5770cc9 100755 --- a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh +++ b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh @@ -23,7 +23,7 @@ ASSET_ID=$(${gcmd} asset info --creator $ACCOUNT --asset bogo|grep 'Asset ID'|aw # Create app that reads asset balance and checks asset details and checks round ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo "int 1") | grep Created | awk '{ print $6 }') +APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --foreign-asset $ASSET_ID --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo "int 1") | grep Created | awk '{ print $6 }') # Create another account, fund it, send it some asset ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') @@ -34,7 +34,7 @@ ${gcmd} asset send --assetid $ASSET_ID -a 17 -f $ACCOUNT -t $ACCOUNTB # Call app from account B, do some checks on asset balance ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:0" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" +${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --foreign-asset $ASSET_ID --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:0" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" # Freeze account B's holding ${gcmd} asset freeze --assetid $ASSET_ID --freeze=true --freezer $ACCOUNT --account $ACCOUNTB @@ -42,4 +42,4 @@ ${gcmd} asset freeze --assetid $ASSET_ID --freeze=true --freezer $ACCOUNT --acco # Check bit flipped ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:1" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" +${gcmd} app call --app-id $APP_ID --from $ACCOUNTB --foreign-asset $ASSET_ID --app-arg "int:$ASSET_ID" --app-arg "int:17" --app-arg "int:1" --app-arg "int:1" --app-arg "str:" --app-arg "str:" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" diff --git a/test/scripts/e2e_subs/tealprogs/assetround.teal b/test/scripts/e2e_subs/tealprogs/assetround.teal index 0b1779d33b..e6b3c6144f 100644 --- a/test/scripts/e2e_subs/tealprogs/assetround.teal +++ b/test/scripts/e2e_subs/tealprogs/assetround.teal @@ -6,7 +6,7 @@ // Check sender's account int 0 -// Check app ID as passed in +// Check asset ID as passed in txna ApplicationArgs 0 btoi @@ -23,7 +23,7 @@ bz fail // Check sender's account int 0 -// Check app ID as passed in +// Check asset ID as passed in txna ApplicationArgs 0 btoi @@ -46,13 +46,9 @@ bnz round // Params // -// Check sender's account (creator) +// Check foreign asset 0 int 0 -// Check app ID as passed in -txna ApplicationArgs 0 -btoi - // Check total against arg 4 asset_params_get AssetTotal pop @@ -61,13 +57,9 @@ btoi == bz fail -// Check sender's account +// Check foreign asset 0 int 0 -// Check app ID as passed in -txna ApplicationArgs 0 -btoi - // Check name against arg 5 asset_params_get AssetUnitName pop From 628f1e9aaf46420d865f41764604693e944aad52 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 13:16:48 -0400 Subject: [PATCH 199/267] add WellFormed and Empty checks for ForeignAssets --- data/transactions/application.go | 3 +++ data/transactions/application_test.go | 2 +- data/transactions/transaction.go | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/transactions/application.go b/data/transactions/application.go index e08b7decbb..851083f3e5 100644 --- a/data/transactions/application.go +++ b/data/transactions/application.go @@ -165,6 +165,9 @@ func (ac *ApplicationCallTxnFields) Empty() bool { if ac.ForeignApps != nil { return false } + if ac.ForeignAssets != nil { + return false + } if ac.LocalStateSchema != (basics.StateSchema{}) { return false } diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 8e9cd0616a..041e841612 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -34,7 +34,7 @@ func TestApplicationCallFieldsNotChanged(t *testing.T) { af := ApplicationCallTxnFields{} s := reflect.ValueOf(&af).Elem() - if s.NumField() != 10 { + if s.NumField() != 11 { t.Errorf("You added or removed a field from ApplicationCallTxnFields. " + "Please ensure you have updated the Empty() method and then " + "fix this test") diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 88539668f6..164017d178 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -365,6 +365,10 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa return fmt.Errorf("tx.ForeignApps too long, max number of foreign apps is %d", proto.MaxAppTxnForeignApps) } + if len(tx.ForeignAssets) > proto.MaxAppTxnForeignAssets { + return fmt.Errorf("tx.ForeignAssets too long, max number of foreign assets is %d", proto.MaxAppTxnForeignAssets) + } + if len(tx.ApprovalProgram) > proto.MaxAppProgramLen { return fmt.Errorf("approval program too long. max len %d bytes", proto.MaxAppProgramLen) } From 59e7a65e108d944648dda008418daf022a602865 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 29 Jul 2020 13:52:46 -0400 Subject: [PATCH 200/267] Test something. --- daemon/algod/api/server/v2/handlers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index d7c612f743..f41ab3db75 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -444,6 +444,7 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response := struct { AssetIndex *uint64 `codec:"asset-index,omitempty"` ApplicationIndex *uint64 `codec:"application-index,omitempty"` + Test *uint64 `codec:"test,omitempty"` CloseRewards *uint64 `codec:"close-rewards,omitempty"` ClosingAmount *uint64 `codec:"closing-amount,omitempty"` ConfirmedRound *uint64 `codec:"confirmed-round,omitempty"` @@ -473,6 +474,8 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.Ledger()) response.ApplicationIndex = computeApplicationIndexFromTxn(txn, v2.Node.Ledger()) + var number uint64 = 5 + response.Test = &number response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn) } From bdc0dddd570879825e2a46504895343ca99341a5 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 14:01:10 -0400 Subject: [PATCH 201/267] fix typo --- data/transactions/logic/evalStateful_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index fa411b4d2e..e99a33ef4e 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -156,7 +156,7 @@ func (l *testLedger) AssetParams(assetID basics.AssetIndex) (basics.AssetParams, if asset, ok := l.assets[assetID]; ok { return asset, nil } - return basics.AssetParams{}, fmt.Errorf("No such asset") + return basics.AssetParams{}, fmt.Errorf("no such asset") } func (l *testLedger) ApplicationID() basics.AppIndex { From e86c5f7ff9378da73c678848680104c4da80a754 Mon Sep 17 00:00:00 2001 From: Will Winder Date: Wed, 29 Jul 2020 14:11:00 -0400 Subject: [PATCH 202/267] Fix app id computation. --- daemon/algod/api/server/v2/handlers.go | 5 +---- daemon/algod/api/server/v2/utils.go | 17 ++++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/daemon/algod/api/server/v2/handlers.go b/daemon/algod/api/server/v2/handlers.go index f41ab3db75..e13736e318 100644 --- a/daemon/algod/api/server/v2/handlers.go +++ b/daemon/algod/api/server/v2/handlers.go @@ -444,7 +444,6 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response := struct { AssetIndex *uint64 `codec:"asset-index,omitempty"` ApplicationIndex *uint64 `codec:"application-index,omitempty"` - Test *uint64 `codec:"test,omitempty"` CloseRewards *uint64 `codec:"close-rewards,omitempty"` ClosingAmount *uint64 `codec:"closing-amount,omitempty"` ConfirmedRound *uint64 `codec:"confirmed-round,omitempty"` @@ -473,9 +472,7 @@ func (v2 *Handlers) PendingTransactionInformation(ctx echo.Context, txid string, response.CloseRewards = &txn.ApplyData.CloseRewards.Raw response.AssetIndex = computeAssetIndexFromTxn(txn, v2.Node.Ledger()) - response.ApplicationIndex = computeApplicationIndexFromTxn(txn, v2.Node.Ledger()) - var number uint64 = 5 - response.Test = &number + response.ApplicationIndex = computeAppIndexFromTxn(txn, v2.Node.Ledger()) response.LocalStateDelta, response.GlobalStateDelta = convertToDeltas(txn) } diff --git a/daemon/algod/api/server/v2/utils.go b/daemon/algod/api/server/v2/utils.go index 9a7994b360..d616fa1010 100644 --- a/daemon/algod/api/server/v2/utils.go +++ b/daemon/algod/api/server/v2/utils.go @@ -85,7 +85,7 @@ func byteOrNil(data []byte) *[]byte { return &data } -func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (aidx *uint64) { +func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, payset []transactions.SignedTxnWithAD) (cidx *uint64) { // Compute transaction index in block offset := -1 for idx, stxnib := range payset { @@ -105,6 +105,8 @@ func computeCreatableIndexInPayset(tx node.TxnWithStatus, txnCounter uint64, pay return &idx } + + // computeAssetIndexFromTxn returns the created asset index given a confirmed // transaction whose confirmation block is available in the ledger. Note that // 0 is an invalid asset index (they start at 1). @@ -140,10 +142,10 @@ func computeAssetIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx *uint return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) } -// computeAssetIndexFromTxn returns the created asset index given a confirmed +// computeAppIndexFromTxn returns the created app index given a confirmed // transaction whose confirmation block is available in the ledger. Note that // 0 is an invalid asset index (they start at 1). -func computeApplicationIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx *uint64) { +func computeAppIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx *uint64) { // Must have ledger if l == nil { return nil @@ -152,12 +154,12 @@ func computeApplicationIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx if tx.ConfirmedRound == 0 { return nil } - // Transaction must be AssetConfig transaction - if tx.Txn.Txn.ApplicationID == 0 { + // Transaction must be ApplicationCall transaction + if tx.Txn.Txn.ApplicationCallTxnFields.Empty() { return nil } - // Transaction must be creating an asset - if tx.Txn.Txn.AssetConfigTxnFields.ConfigAsset != 0 { + // Transaction must be creating an application + if tx.Txn.Txn.ApplicationCallTxnFields.ApplicationID != 0 { return nil } @@ -175,6 +177,7 @@ func computeApplicationIndexFromTxn(tx node.TxnWithStatus, l *data.Ledger) (aidx return computeCreatableIndexInPayset(tx, blk.BlockHeader.TxnCounter, payset) } + // getCodecHandle converts a format string into the encoder + content type func getCodecHandle(formatPtr *string) (codec.Handle, string, error) { format := "json" From eb4280db611c6ef1e0645be51cd8ab1a520225a1 Mon Sep 17 00:00:00 2001 From: ryanRfox Date: Wed, 29 Jul 2020 14:11:46 -0400 Subject: [PATCH 203/267] fixup docs --- cmd/goal/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/goal/node.go b/cmd/goal/node.go index 2ae52640a6..95f2a6c0cd 100644 --- a/cmd/goal/node.go +++ b/cmd/goal/node.go @@ -117,7 +117,7 @@ var nodeCmd = &cobra.Command{ var catchupCmd = &cobra.Command{ Use: "catchup", Short: "Catchup the Algorand node to a specific catchpoint", - Long: "Catchup allows making large jumps over round ranges without the need to incremently validate each individual round.", + Long: "Catchup allows making large jumps over round ranges without the need to incrementally validate each individual round.", Example: "goal node catchup 6500000#1234567890ABCDEF01234567890ABCDEF0\tStart catching up to round 6500000 with the provided catchpoint\ngoal node catchup --abort\t\t\t\t\tAbort the current catchup", Args: catchpointCmdArgument, Run: func(cmd *cobra.Command, args []string) { From 9ba1bd100497c192d36c18516135494751da396e Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Wed, 29 Jul 2020 15:32:36 -0400 Subject: [PATCH 204/267] Move the go-deadlock to use go modules --- go.mod | 3 +-- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 85466178e1..b1bddad3ce 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.12 require ( github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d - github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 + github.com/algorand/go-deadlock v0.2.1 github.com/algorand/msgp v1.1.45 github.com/algorand/oapi-codegen v1.3.5-algorand5 github.com/algorand/websocket v1.4.1 @@ -38,7 +38,6 @@ require ( github.com/olivere/elastic v6.2.14+incompatible github.com/onsi/ginkgo v1.8.0 // indirect github.com/onsi/gomega v1.5.0 // indirect - github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/russross/blackfriday v1.5.2 // indirect github.com/satori/go.uuid v1.2.0 github.com/sirupsen/logrus v1.0.5 diff --git a/go.sum b/go.sum index 869b3333f4..918b6e2712 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/algorand/go-codec v1.1.2 h1:QWS9YC3EEWBpJq5AqFPELcCJ2QPpTIg9aqR2K/sRD github.com/algorand/go-codec v1.1.2/go.mod h1:A3YI4V24jUUnU1eNekNmx2fLi60FvlNssqOiUsyfNM8= github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d h1:W9MgGUodEl4Y4+CxeEr+T3fZ26kOcWA4yfqhjbFxxmI= github.com/algorand/go-codec/codec v0.0.0-20190507210007-269d70b6135d/go.mod h1:qm6LyXvDa1+uZJxaVg8X+OEjBqt/zDinDa2EohtTDxU= -github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759 h1:IiCuOE1YCReVyEr1IQHKTBTvFLKdeBCfQuxrqhniq+I= -github.com/algorand/go-deadlock v0.0.0-20181221160745-78d8cb5e2759/go.mod h1:Kve3O9VpxZIHsPzpfxNdyFltFU9jBTeVYMYxSC99tdg= +github.com/algorand/go-deadlock v0.2.1 h1:TQPQwWAB133bS5uwHpmrgH5hCMyZK5hnUW26aqWMvq4= +github.com/algorand/go-deadlock v0.2.1/go.mod h1:HgdF2cwtBIBCL7qmUaozuG/UIZFR6PLpSMR58pvWiXE= github.com/algorand/msgp v1.1.45 h1:uLEPvg0BfTrb2JjBcXexON8il4vv0EsD7HYFaA7e5jg= github.com/algorand/msgp v1.1.45/go.mod h1:LtOntbYiCHj/Sl/Sqxtf8CZOrDt2a8Dv3tLaS6mcnUE= github.com/algorand/oapi-codegen v1.3.5-algorand5 h1:y576Ca2/guQddQrQA7dtL5KcOx5xQgPeIupiuFMGyCI= From 7de26ae000c755d064105709a33a8d64cb602144 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 16:17:46 -0400 Subject: [PATCH 205/267] fix test --- cmd/tealdbg/local_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index 26aa5aa765..b81d554f1e 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -316,6 +316,7 @@ func TestDebugEnvironment(t *testing.T) { ApplicationArgs: [][]byte{[]byte("ALGO"), []byte("RAND")}, Accounts: []basics.Address{receiver}, ForeignApps: []basics.AppIndex{appIdx1}, + ForeignAssets: []basics.AssetIndex{assetIdx}, }, }, } @@ -434,7 +435,6 @@ int 10 == && int 0 -int 50 asset_params_get AssetTotal bnz ok4 err From 5e97f40ef29aff399c3fa3f6abcf9ebfe3623152 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 16:32:36 -0400 Subject: [PATCH 206/267] update assembler tests --- data/transactions/logic/assembler_test.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 445df23f50..43bc1da1dd 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -182,7 +182,6 @@ asset_holding_get AssetBalance pop pop int 0 -int 1 asset_params_get AssetTotal pop pop @@ -239,7 +238,7 @@ func TestAssemble(t *testing.T) { program, err := AssembleStringWithVersion(bigTestAssembleNonsenseProgram, AssemblerMaxVersion) require.NoError(t, err) // check that compilation is stable over time and we assemble to the same bytes this month that we did last month. - expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b6921072105700048482107210571004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f") + expectedBytes, _ := hex.DecodeString("022008b7a60cf8acd19181cf959a12f8acd19181cf951af8acd19181cf15f8acd191810f01020026050212340c68656c6c6f20776f726c6421208dae2087fbba51304eb02b91f656948397a7946390e8cb70fc9ea4d95f92251d024242047465737400320032013202320328292929292a0431003101310231043105310731083109310a310b310c310d310e310f3111311231133114311533000033000133000233000433000533000733000833000933000a33000b33000c33000d33000e33000f3300113300123300133300143300152d2e0102222324252104082209240a220b230c240d250e230f23102311231223132314181b1c2b171615400003290349483403350222231d4a484848482a50512a63222352410003420000432105602105612105270463484821052b62482b642b65484821052b2106662b21056721072b682b692107210570004848210771004848361c0037001a0031183119311b311d311e311f3120210721051e312131223123312431253126312731283129312a312b312c312d312e312f") if bytes.Compare(expectedBytes, program) != 0 { // this print is for convenience if the program has been changed. the hex string can be copy pasted back in as a new expected result. t.Log(hex.EncodeToString(program)) @@ -993,15 +992,15 @@ func TestAssembleDisassembleErrors(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "invalid asset holding arg index") - source = "int 0\nint 0\nasset_params_get AssetTotal" + source = "int 0\nasset_params_get AssetTotal" program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) - program[7] = 0x50 // params field + program[6] = 0x50 // params field _, err = Disassemble(program) require.Error(t, err) require.Contains(t, err.Error(), "invalid asset params arg index") - source = "int 0\nint 0\nasset_params_get AssetTotal" + source = "int 0\nasset_params_get AssetTotal" program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.NoError(t, err) _, err = Disassemble(program) @@ -1082,12 +1081,12 @@ func TestAssembleAsset(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "asset_holding_get unknown arg") - source = "int 0\nint 0\nasset_params_get ABC 1" + source = "int 0\nasset_params_get ABC 1" _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_params_get expects one argument") - source = "int 0\nint 0\nasset_params_get ABC" + source = "int 0\nasset_params_get ABC" _, err = AssembleStringWithVersion(source, AssemblerMaxVersion) require.Error(t, err) require.Contains(t, err.Error(), "asset_params_get unknown arg") From c7e6948b5fc67388e583b6e3cb30c9b81f729f24 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Wed, 29 Jul 2020 18:58:01 -0400 Subject: [PATCH 207/267] fix ledger app test --- ledger/applications_test.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ledger/applications_test.go b/ledger/applications_test.go index e75fe80393..b7bbf90f61 100644 --- a/ledger/applications_test.go +++ b/ledger/applications_test.go @@ -28,8 +28,9 @@ import ( ) type testBalances struct { - appCreators map[basics.AppIndex]basics.Address - balances map[basics.Address]basics.AccountData + appCreators map[basics.AppIndex]basics.Address + assetCreators map[basics.AssetIndex]basics.Address + balances map[basics.Address]basics.AccountData } type testBalancesPass struct { @@ -65,6 +66,11 @@ func (b *testBalances) GetCreator(cidx basics.CreatableIndex, ctype basics.Creat creator, ok := b.appCreators[aidx] return creator, ok, nil } + if ctype == basics.AssetCreatable { + aidx := basics.AssetIndex(cidx) + creator, ok := b.assetCreators[aidx] + return creator, ok, nil + } return basics.Address{}, false, nil } @@ -183,6 +189,7 @@ func TestAppLedgerAsset(t *testing.T) { assetIdx := basics.AssetIndex(2) b.balances = map[basics.Address]basics.AccountData{addr1: {}} + b.assetCreators = map[basics.AssetIndex]basics.Address{assetIdx: addr1} _, err = l.AssetParams(assetIdx) a.Error(err) a.Contains(err.Error(), "has not created asset") From e5bf1581a2c3b9e561762a8fc791a49bff7f2600 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 11:07:29 -0400 Subject: [PATCH 208/267] remove test that fails on travis but not locally, update TestApplicationCallFieldsEmpty --- data/transactions/application_test.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index 041e841612..07d97cc7dc 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -19,7 +19,6 @@ package transactions import ( "fmt" "math/rand" - "reflect" "testing" "github.com/stretchr/testify/require" @@ -30,17 +29,6 @@ import ( "github.com/algorand/go-algorand/protocol" ) -func TestApplicationCallFieldsNotChanged(t *testing.T) { - af := ApplicationCallTxnFields{} - s := reflect.ValueOf(&af).Elem() - - if s.NumField() != 11 { - t.Errorf("You added or removed a field from ApplicationCallTxnFields. " + - "Please ensure you have updated the Empty() method and then " + - "fix this test") - } -} - func TestApplicationCallFieldsEmpty(t *testing.T) { a := require.New(t) @@ -67,6 +55,10 @@ func TestApplicationCallFieldsEmpty(t *testing.T) { a.False(ac.Empty()) ac.ForeignApps = nil + ac.ForeignAssets = make([]basics.AssetIndex, 1) + a.False(ac.Empty()) + + ac.ForeignAssets = nil ac.LocalStateSchema = basics.StateSchema{NumUint: 1} a.False(ac.Empty()) From 3f6c07520cc674ed5804788bbf87476f48a653ee Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 11:22:43 -0400 Subject: [PATCH 209/267] add api v1 support for ForeignAssets --- daemon/algod/api/server/v1/handlers/handlers.go | 6 ++++++ daemon/algod/api/spec/v1/model.go | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/daemon/algod/api/server/v1/handlers/handlers.go b/daemon/algod/api/server/v1/handlers/handlers.go index 3734aea644..313d8eb20f 100644 --- a/daemon/algod/api/server/v1/handlers/handlers.go +++ b/daemon/algod/api/server/v1/handlers/handlers.go @@ -278,6 +278,11 @@ func applicationCallTxEncode(tx transactions.Transaction, ad transactions.ApplyD encodedForeignApps = append(encodedForeignApps, uint64(aidx)) } + encodedForeignAssets := make([]uint64, 0, len(tx.ForeignAssets)) + for _, aidx := range tx.ForeignAssets { + encodedForeignAssets = append(encodedForeignAssets, uint64(aidx)) + } + encodedArgs := make([]string, 0, len(tx.ApplicationArgs)) for _, arg := range tx.ApplicationArgs { encodedArgs = append(encodedArgs, b64.EncodeToString([]byte(arg))) @@ -286,6 +291,7 @@ func applicationCallTxEncode(tx transactions.Transaction, ad transactions.ApplyD app.Accounts = encodedAccounts app.ApplicationArgs = encodedArgs app.ForeignApps = encodedForeignApps + app.ForeignAssets = encodedForeignAssets return v1.Transaction{ ApplicationCall: &app, } diff --git a/daemon/algod/api/spec/v1/model.go b/daemon/algod/api/spec/v1/model.go index 7f70e5f82a..1ccbeb5cc2 100644 --- a/daemon/algod/api/spec/v1/model.go +++ b/daemon/algod/api/spec/v1/model.go @@ -713,6 +713,13 @@ type ApplicationCallTransactionType struct { // required: true ForeignApps []uint64 `json:"foreignapps"` + // ForeignAssets lists the assets whose parameters may be accessed by + // this application's ApprovalProgram and ClearStateProgram. The access + // is read-only. + // + // required: true + ForeignAssets []uint64 `json:"foreignassets"` + // ApplicationArgs lists some transaction-specific arguments accessible // from application logic // From 69986908f0bac86c5ee359e39e9ed3a2f496767a Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 11:50:16 -0400 Subject: [PATCH 210/267] disallow TEAL v0 and v1 approval/clearstate programs --- data/transactions/logic/eval.go | 21 +++++++++++++++++++++ data/transactions/logic/opcodes.go | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index b5341e0f0a..b417775ac6 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -386,6 +386,11 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { return } + err = cx.checkApplicationsAllowed() + if err != nil { + return + } + if cx.Debugger != nil { cx.debugState = makeDebugState(cx) if err = cx.Debugger.Register(cx.refreshDebugState()); err != nil { @@ -487,6 +492,11 @@ func check(program []byte, params EvalParams) (cost int, err error) { return } + err = cx.checkApplicationsAllowed() + if err != nil { + return + } + for (cx.err == nil) && (cx.pc < len(cx.program)) { prevpc := cx.pc cost += cx.checkStep() @@ -512,6 +522,17 @@ func (cx *evalContext) checkRekeyAllowed() error { return nil } +func (cx *evalContext) checkApplicationsAllowed() error { + // Require the use of TEAL v2 or greater for an application's + // ApprovalProgram or ClearStateProgram. + if cx.version < appsEnabledVersion && (cx.runModeFlags&runModeApplication) != 0 { + // Don't prompt to just use `#pragma version 2`, because that + // might not be safe, depending on the program. + return fmt.Errorf("program version must be at least %d to be used with Applications", appsEnabledVersion) + } + return nil +} + func opCompat(expected, got StackType) bool { if expected == StackAny { return true diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index a2dbc29ef5..a4dc0fe13d 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -28,6 +28,11 @@ const LogicVersion = 2 // be maliciously or accidentally rekeyed. const rekeyingEnabledVersion = 2 +// appsEnabledVersion is the version of TEAL where ApplicationCall +// functionality was enabled. We use this to disallow v0 and v1 TEAL programs +// from being used with applications. +const appsEnabledVersion = 2 + // opSize records the length in bytes for an op that is constant-length but not length 1 type opSize struct { cost int From 7b7083a1824cd87cfbb04346567ac2c944d9f477 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 30 Jul 2020 12:08:52 -0400 Subject: [PATCH 211/267] Add missing deadlock disable updates on TestArchivalCreatables --- ledger/archival_test.go | 13 +++++++++++-- ledger/ledger_perf_test.go | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ledger/archival_test.go b/ledger/archival_test.go index cb6b54530d..ddb32b4957 100644 --- a/ledger/archival_test.go +++ b/ledger/archival_test.go @@ -172,9 +172,10 @@ func TestArchivalRestart(t *testing.T) { // Start in archival mode, add 2K blocks, restart, ensure all blocks are there // disable deadlock checking code + deadlockDisable := deadlock.Opts.Disable deadlock.Opts.Disable = true defer func() { - deadlock.Opts.Disable = false + deadlock.Opts.Disable = deadlockDisable }() dbTempDir, err := ioutil.TempDir("", "testdir"+t.Name()) @@ -318,6 +319,13 @@ func TestArchivalCreatables(t *testing.T) { // restart, ensure all assets are there in index unless they were // deleted + // disable deadlock checking code + deadlockDisable := deadlock.Opts.Disable + deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() + dbTempDir, err := ioutil.TempDir("", "testdir"+t.Name()) require.NoError(t, err) dbName := fmt.Sprintf("%s.%d", t.Name(), crypto.RandUint64()) @@ -664,9 +672,10 @@ func makeSignedTxnInBlock(tx transactions.Transaction) transactions.SignedTxnInB func TestArchivalFromNonArchival(t *testing.T) { // Start in non-archival mode, add 2K blocks, restart in archival mode ensure only genesis block is there + deadlockDisable := deadlock.Opts.Disable deadlock.Opts.Disable = true defer func() { - deadlock.Opts.Disable = false + deadlock.Opts.Disable = deadlockDisable }() dbTempDir, err := ioutil.TempDir(os.TempDir(), "testdir") require.NoError(t, err) diff --git a/ledger/ledger_perf_test.go b/ledger/ledger_perf_test.go index c25e6e5c12..d1bd9ebea5 100644 --- a/ledger/ledger_perf_test.go +++ b/ledger/ledger_perf_test.go @@ -135,9 +135,12 @@ func (vc *alwaysVerifiedCache) Verified(txn transactions.SignedTxn, params verif } func benchmarkFullBlocks(params testParams, b *testing.B) { - // Disable deadlock checking library (we do this here and not in init - // because we want deadlock detection for unit tests) + // disable deadlock checking code + deadlockDisable := deadlock.Opts.Disable deadlock.Opts.Disable = true + defer func() { + deadlock.Opts.Disable = deadlockDisable + }() dbTempDir, err := ioutil.TempDir("", "testdir"+b.Name()) require.NoError(b, err) From a7d6ef90e8ce8e12d34428822395a0bf9209debd Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 30 Jul 2020 12:30:42 -0400 Subject: [PATCH 212/267] update error messages. --- ledger/acctupdates.go | 8 ++++---- ledger/catchupaccessor.go | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 3445d5a15a..bc796d3c44 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -19,6 +19,7 @@ package ledger import ( "context" "database/sql" + "encoding/hex" "fmt" "io" "os" @@ -854,7 +855,7 @@ func (au *accountUpdates) accountsInitialize(ctx context.Context, tx *sql.Tx) (b return rnd, fmt.Errorf("accountsInitialize was unable to add changes to trie: %v", err) } if !added { - au.log.Warnf("accountsInitialize attempted to add duplicate hash '%v' to merkle trie.", hash) + au.log.Warnf("accountsInitialize attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(hash), balance.Address) } } @@ -1032,11 +1033,10 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basic return err } if !deleted { - au.log.Warnf("failed to delete hash '%v' from merkle trie", deleteHash) + au.log.Warnf("failed to delete hash '%s' from merkle trie for account %v", hex.EncodeToString(deleteHash), addr) } else { accumulatedChanges++ } - } if !delta.new.IsZero() { addHash := accountHashBuilder(addr, delta.new, protocol.Encode(&delta.new)) @@ -1045,7 +1045,7 @@ func (au *accountUpdates) accountsUpdateBalances(accountsDeltasRound []map[basic return err } if !added { - au.log.Warnf("attempted to add duplicate hash '%v' to merkle trie", addHash) + au.log.Warnf("attempted to add duplicate hash '%s' to merkle trie for account %v", hex.EncodeToString(addHash), addr) } else { accumulatedChanges++ } diff --git a/ledger/catchupaccessor.go b/ledger/catchupaccessor.go index a321d3056b..b3a4a7968b 100644 --- a/ledger/catchupaccessor.go +++ b/ledger/catchupaccessor.go @@ -19,6 +19,7 @@ package ledger import ( "context" "database/sql" + "encoding/hex" "fmt" "strings" @@ -331,7 +332,7 @@ func (c *CatchpointCatchupAccessorImpl) processStagingBalances(ctx context.Conte hash := accountHashBuilder(balance.Address, accountData, balance.AccountData) added, err := trie.Add(hash) if !added { - return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingBalances: The provided catchpoint file contained the same account more than once. Account address %#v, account data %#v", balance.Address, accountData) + return fmt.Errorf("CatchpointCatchupAccessorImpl::processStagingBalances: The provided catchpoint file contained the same account more than once. Account address %#v, account data %#v, hash '%s'", balance.Address, accountData, hex.EncodeToString(hash)) } if err != nil { return err From d849f4b07bd6f06e8454680545bd31dc4cd794df Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 12:42:12 -0400 Subject: [PATCH 213/267] fix some tests --- .../transactions/logic/backwardCompat_test.go | 12 +++---- data/transactions/logic/eval_test.go | 31 ++++++++++++++++--- test/scripts/e2e_subs/e2e-app-bootloader.sh | 3 +- test/scripts/e2e_subs/e2e-app-simple.sh | 2 +- .../e2e_subs/e2e-app-stateful-global.sh | 3 +- .../e2e_subs/e2e-app-stateful-local.sh | 3 +- test/scripts/e2e_subs/e2e-teal.sh | 1 + 7 files changed, 41 insertions(+), 14 deletions(-) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index d745dd746e..bb6e63914f 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -371,7 +371,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "greater than protocol supported version") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "greater than protocol supported version") @@ -380,7 +380,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid global[") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid global[") @@ -389,7 +389,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid global[") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid global[") } @@ -459,7 +459,7 @@ func TestBackwardCompatTxnFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "greater than protocol supported version") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "greater than protocol supported version") @@ -468,7 +468,7 @@ func TestBackwardCompatTxnFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid txn field") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid txn field") @@ -477,7 +477,7 @@ func TestBackwardCompatTxnFields(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid txn field") - _, _, err = EvalStateful(program, ep) + _, err = Eval(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid txn field") } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 314f4318d9..8507ed1c68 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3652,6 +3652,32 @@ func TestArgType(t *testing.T) { require.Equal(t, StackUint64, sv.argType()) } +func TestApplicationsDisallowOldTeal(t *testing.T) { + const source = "int 1" + ep := defaultEvalParams(nil, nil) + for v := uint64(0); v < appsEnabledVersion; v++ { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + + _, err = CheckStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "program version must be at least") + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), "program version must be at least") + } + + program, err := AssembleStringWithVersion(source, appsEnabledVersion) + require.NoError(t, err) + + _, err = CheckStateful(program, ep) + require.NoError(t, err) + + _, _, err = EvalStateful(program, ep) + require.NoError(t, err) +} + // check all v2 opcodes: allowed in v2 and not allowed in v1 and v0 func TestAllowedOpcodesV2(t *testing.T) { t.Parallel() @@ -3716,7 +3742,7 @@ func TestAllowedOpcodesV2(t *testing.T) { strings.Contains(err.Error(), "illegal opcode") || strings.Contains(err.Error(), "pc did not advance"), ) - _, err = CheckStateful(program, ep) + _, err = Check(program, ep) require.Error(t, err, source) require.True(t, strings.Contains(err.Error(), "illegal opcode") || @@ -3725,9 +3751,6 @@ func TestAllowedOpcodesV2(t *testing.T) { _, err = Eval(program, ep) require.Error(t, err, source) require.Contains(t, err.Error(), "illegal opcode") - _, _, err = EvalStateful(program, ep) - require.Error(t, err, source) - require.Contains(t, err.Error(), "illegal opcode") } cnt++ } diff --git a/test/scripts/e2e_subs/e2e-app-bootloader.sh b/test/scripts/e2e_subs/e2e-app-bootloader.sh index 9019bbc8b8..0759f24db3 100755 --- a/test/scripts/e2e_subs/e2e-app-bootloader.sh +++ b/test/scripts/e2e_subs/e2e-app-bootloader.sh @@ -32,7 +32,8 @@ sed -i"" -e "s/TMPL_APPROV_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal sed -i"" -e "s/TMPL_CLEARSTATE_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal # Create an app using filled-in bootloader template -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${TEMPDIR}/bootloader.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') +echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${TEMPDIR}/bootloader.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Calling app without args and wrong OnCompletion should fail EXPERROR='rejected by ApprovalProgram' diff --git a/test/scripts/e2e_subs/e2e-app-simple.sh b/test/scripts/e2e_subs/e2e-app-simple.sh index 80e8e9add9..4243324cdf 100755 --- a/test/scripts/e2e_subs/e2e-app-simple.sh +++ b/test/scripts/e2e_subs/e2e-app-simple.sh @@ -14,7 +14,7 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') GLOBAL_INTS=2 -echo 'int 1' > "${TEMPDIR}/simple.teal" +echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" PROGRAM=($(${gcmd} clerk compile "${TEMPDIR}/simple.teal")) # Succeed in creating app that approves all transactions diff --git a/test/scripts/e2e_subs/e2e-app-stateful-global.sh b/test/scripts/e2e_subs/e2e-app-stateful-global.sh index f09802ce12..9248061c74 100755 --- a/test/scripts/e2e_subs/e2e-app-stateful-global.sh +++ b/test/scripts/e2e_subs/e2e-app-stateful-global.sh @@ -17,7 +17,8 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Succeed in creating app that approves transactions with arg[0] == 'hello' -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globcheck.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:hello" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') +echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globcheck.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:hello" --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Application call with no args should fail EXPERROR='invalid ApplicationArgs index 0' diff --git a/test/scripts/e2e_subs/e2e-app-stateful-local.sh b/test/scripts/e2e_subs/e2e-app-stateful-local.sh index dda8f00e4b..a7a1099380 100755 --- a/test/scripts/e2e_subs/e2e-app-stateful-local.sh +++ b/test/scripts/e2e_subs/e2e-app-stateful-local.sh @@ -17,7 +17,8 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Succeed in creating app that approves transactions with arg[0] == 'hello' -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/loccheck.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --app-arg "str:hello" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') +echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/loccheck.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --app-arg "str:hello" --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Application call with no args should fail EXPERROR='invalid ApplicationArgs index 0' diff --git a/test/scripts/e2e_subs/e2e-teal.sh b/test/scripts/e2e_subs/e2e-teal.sh index 534f3b91e8..59517b336c 100755 --- a/test/scripts/e2e_subs/e2e-teal.sh +++ b/test/scripts/e2e_subs/e2e-teal.sh @@ -72,6 +72,7 @@ done ${gcmd} clerk send --from-program ${TEMPDIR}/tlhc.teal --to ${ACCOUNT} --close-to ${ACCOUNT} --amount 1 --argb64 AA== cat >${TEMPDIR}/true.teal< Date: Thu, 30 Jul 2020 13:25:34 -0400 Subject: [PATCH 214/267] fix duplicate unit test --- data/transactions/application_test.go | 12 ++++++++++++ ledger/apply/application_test.go | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/data/transactions/application_test.go b/data/transactions/application_test.go index c55b9bc437..82e3f6d7d3 100644 --- a/data/transactions/application_test.go +++ b/data/transactions/application_test.go @@ -17,6 +17,7 @@ package transactions import ( + "reflect" "testing" "github.com/stretchr/testify/require" @@ -25,6 +26,17 @@ import ( "github.com/algorand/go-algorand/data/basics" ) +func TestApplicationCallFieldsNotChanged(t *testing.T) { + af := ApplicationCallTxnFields{} + s := reflect.ValueOf(&af).Elem() + + if s.NumField() != 11 { + t.Errorf("You added or removed a field from transactions.ApplicationCallTxnFields. " + + "Please ensure you have updated the Empty() method and then " + + "fix this test") + } +} + func TestApplicationCallFieldsEmpty(t *testing.T) { a := require.New(t) diff --git a/ledger/apply/application_test.go b/ledger/apply/application_test.go index 21aedcc6c8..03edb3721a 100644 --- a/ledger/apply/application_test.go +++ b/ledger/apply/application_test.go @@ -19,7 +19,6 @@ package apply import ( "fmt" "math/rand" - "reflect" "testing" "github.com/stretchr/testify/require" @@ -31,17 +30,6 @@ import ( "github.com/algorand/go-algorand/protocol" ) -func TestApplicationCallFieldsNotChanged(t *testing.T) { - af := transactions.ApplicationCallTxnFields{} - s := reflect.ValueOf(&af).Elem() - - if s.NumField() != 10 { - t.Errorf("You added or removed a field from transactions.ApplicationCallTxnFields. " + - "Please ensure you have updated the Empty() method and then " + - "fix this test") - } -} - func TestApplicationCallFieldsEmpty(t *testing.T) { a := require.New(t) From 49e271e752562a29c449f45f1ca37928ca0228b7 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 13:26:55 -0400 Subject: [PATCH 215/267] remove extra debug output --- data/transactions/logic/evalStateful_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index e99a33ef4e..611d076acc 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -1140,11 +1140,9 @@ int 1 pass, _, err = EvalStateful(program, ep) require.NoError(t, err) require.True(t, pass) - t.Logf("%x", program) // check holdings invalid offsets require.Equal(t, opsByName[ep.Proto.LogicSigVersion]["asset_params_get"].Opcode, program[6]) program[7] = 0x20 - t.Logf("%x", program) _, _, err = EvalStateful(program, ep) require.Error(t, err) require.Contains(t, err.Error(), "invalid asset params field 32") From 9ac08517c8fd5b96cd308ed33f690bf096da7b19 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 14:20:29 -0400 Subject: [PATCH 216/267] add MinTealVersion to TEAL --- .../transactions/logic/backwardCompat_test.go | 5 + data/transactions/logic/eval.go | 94 ++++++++++--------- data/transactions/logic/eval_test.go | 17 +++- data/transactions/verify/txn.go | 20 ++-- ledger/eval.go | 11 ++- 5 files changed, 85 insertions(+), 62 deletions(-) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index bb6e63914f..c00632941f 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -277,6 +277,8 @@ func TestBackwardCompatTEALv1(t *testing.T) { }) txn := makeSampleTxn() + // RekeyTo disallowed on TEAL v0/v1 + txn.Txn.RekeyTo = basics.Address{} txgroup := makeSampleTxnGroup(txn) txn.Lsig.Logic = program txn.Lsig.Args = [][]byte{data[:], sig[:], pk[:], txn.Txn.Sender[:], txn.Txn.Note} @@ -455,6 +457,9 @@ func TestBackwardCompatTxnFields(t *testing.T) { ep.Ledger = ledger ep.TxnGroup = txgroup + var minTealVersion uint64 + ep.MinTealVersion = &minTealVersion + // check failure with version check _, err = Eval(program, ep) require.Error(t, err) diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index b417775ac6..76a40e41b3 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -91,6 +91,29 @@ func stackValueFromTealValue(tv *basics.TealValue) (sv stackValue, err error) { return } +// ComputeMinTealVersion calculates the minimum safe TEAL version that may be +// used by a transaction in this group. It is important to prevent +// newly-introduced transaction fields from breaking assumptions made by older +// versions of TEAL. If one of the transactions in a group will execute a TEAL +// program whose version predates a given field, that field must not be set +// anywhere in the transaction group, or the group will be rejected. +func ComputeMinTealVersion(group []transactions.SignedTxn) uint64 { + var minVersion uint64 + for _, txn := range group { + if !txn.Txn.RekeyTo.IsZero() { + if minVersion < rekeyingEnabledVersion { + minVersion = rekeyingEnabledVersion + } + } + if txn.Txn.Type == protocol.ApplicationCallTx { + if minVersion < appsEnabledVersion { + minVersion = appsEnabledVersion + } + } + } + return minVersion +} + func (sv *stackValue) toTealValue() (tv basics.TealValue) { if sv.argType() == StackBytes { return basics.TealValue{Type: basics.TealBytesType, Bytes: string(sv.Bytes)} @@ -133,6 +156,11 @@ type EvalParams struct { // optional debugger Debugger DebuggerHook + // MinTealVersion is the minimum allowed TEAL version of this program. + // The program must reject if its version is less than this version. If + // MinTealVersion is nil, we will compute it ourselves + MinTealVersion *uint64 + // determines eval mode: runModeSignature or runModeApplication runModeFlags runMode } @@ -371,26 +399,22 @@ func eval(program []byte, cx *evalContext) (pass bool, err error) { return false, cx.err } - // TODO: if EvalMaxVersion > version, ensure that inaccessible - // fields as of the program's version are zero or other - // default value so that no one is hiding unexpected - // operations from an old program. + var minVersion uint64 + if cx.EvalParams.MinTealVersion == nil { + minVersion = ComputeMinTealVersion(cx.EvalParams.TxnGroup) + } else { + minVersion = *cx.EvalParams.MinTealVersion + } + if version < minVersion { + err = fmt.Errorf("program version must be >= %d for this transaction group, but have version %d", minVersion, version) + return + } cx.version = version cx.pc = vlen cx.stack = make([]stackValue, 0, 10) cx.program = program - err = cx.checkRekeyAllowed() - if err != nil { - return - } - - err = cx.checkApplicationsAllowed() - if err != nil { - return - } - if cx.Debugger != nil { cx.debugState = makeDebugState(cx) if err = cx.Debugger.Register(cx.refreshDebugState()); err != nil { @@ -482,21 +506,22 @@ func check(program []byte, params EvalParams) (cost int, err error) { return } + var minVersion uint64 + if params.MinTealVersion == nil { + minVersion = ComputeMinTealVersion(params.TxnGroup) + } else { + minVersion = *params.MinTealVersion + } + if version < minVersion { + err = fmt.Errorf("program version must be >= %d for this transaction group, but have version %d", minVersion, version) + return + } + cx.version = version cx.pc = vlen cx.EvalParams = params cx.program = program - err = cx.checkRekeyAllowed() - if err != nil { - return - } - - err = cx.checkApplicationsAllowed() - if err != nil { - return - } - for (cx.err == nil) && (cx.pc < len(cx.program)) { prevpc := cx.pc cost += cx.checkStep() @@ -512,27 +537,6 @@ func check(program []byte, params EvalParams) (cost int, err error) { return } -func (cx *evalContext) checkRekeyAllowed() error { - // A transaction may not have a nonzero RekeyTo field set before TEAL - // v2, otherwise TEAL v0 or v1 accounts could be rekeyed by an anyone - // who could ordinarily spend from them. - if cx.version < rekeyingEnabledVersion && !cx.EvalParams.Txn.Txn.RekeyTo.IsZero() { - return fmt.Errorf("program version %d doesn't allow transactions with nonzero RekeyTo field", cx.version) - } - return nil -} - -func (cx *evalContext) checkApplicationsAllowed() error { - // Require the use of TEAL v2 or greater for an application's - // ApprovalProgram or ClearStateProgram. - if cx.version < appsEnabledVersion && (cx.runModeFlags&runModeApplication) != 0 { - // Don't prompt to just use `#pragma version 2`, because that - // might not be safe, depending on the program. - return fmt.Errorf("program version must be at least %d to be used with Applications", appsEnabledVersion) - } - return nil -} - func opCompat(expected, got StackType) bool { if expected == StackAny { return true diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 8507ed1c68..b00e3c448c 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -1890,7 +1890,6 @@ int 1 require.NoError(t, err) require.True(t, cost < 1000) txn := makeSampleTxn() - txgroup := makeSampleTxnGroup(txn) // RekeyTo not allowed in TEAL v1 if v < rekeyingEnabledVersion { txn.Txn.RekeyTo = basics.Address{} @@ -1904,6 +1903,7 @@ int 1 txn.Txn.SelectionPK[:], txn.Txn.Note, } + txgroup := makeSampleTxnGroup(txn) sb = strings.Builder{} ep := defaultEvalParams(&sb, &txn) ep.TxnGroup = txgroup @@ -3655,17 +3655,23 @@ func TestArgType(t *testing.T) { func TestApplicationsDisallowOldTeal(t *testing.T) { const source = "int 1" ep := defaultEvalParams(nil, nil) + + txn := makeSampleTxn() + txn.Txn.Type = protocol.ApplicationCallTx + txngroup := []transactions.SignedTxn{txn} + ep.TxnGroup = txngroup + for v := uint64(0); v < appsEnabledVersion; v++ { program, err := AssembleStringWithVersion(source, v) require.NoError(t, err) _, err = CheckStateful(program, ep) require.Error(t, err) - require.Contains(t, err.Error(), "program version must be at least") + require.Contains(t, err.Error(), fmt.Sprintf("program version must be >= %d", appsEnabledVersion)) _, _, err = EvalStateful(program, ep) require.Error(t, err) - require.Contains(t, err.Error(), "program version must be at least") + require.Contains(t, err.Error(), fmt.Sprintf("program version must be >= %d", appsEnabledVersion)) } program, err := AssembleStringWithVersion(source, appsEnabledVersion) @@ -3770,13 +3776,14 @@ func TestRekeyFailsOnOldVersion(t *testing.T) { sb := strings.Builder{} proto := defaultEvalProto() ep := defaultEvalParams(&sb, &txn) + ep.TxnGroup = []transactions.SignedTxn{txn} ep.Proto = &proto _, err = Check(program, ep) require.Error(t, err) - require.Contains(t, err.Error(), "nonzero RekeyTo field") + require.Contains(t, err.Error(), fmt.Sprintf("program version must be >= %d", rekeyingEnabledVersion)) pass, err := Eval(program, ep) require.Error(t, err) - require.Contains(t, err.Error(), "nonzero RekeyTo field") + require.Contains(t, err.Error(), fmt.Sprintf("program version must be >= %d", rekeyingEnabledVersion)) require.False(t, pass) }) } diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index aa1b0e7325..90e560c081 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -41,8 +41,9 @@ var logicErrTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_l // on a signed transaction. type Context struct { Params - Group []transactions.SignedTxn - GroupIndex int + Group []transactions.SignedTxn + GroupIndex int + MinTealVersion uint64 } // Params is the set of parameters external to a transaction which @@ -62,6 +63,7 @@ type Params struct { // group. func PrepareContexts(group []transactions.SignedTxn, contextHdr bookkeeping.BlockHeader) []Context { ctxs := make([]Context, len(group)) + minTealVersion := logic.ComputeMinTealVersion(group) for i := range group { spec := transactions.SpecialAddresses{ FeeSink: contextHdr.FeeSink, @@ -72,8 +74,9 @@ func PrepareContexts(group []transactions.SignedTxn, contextHdr bookkeeping.Bloc CurrSpecAddrs: spec, CurrProto: contextHdr.CurrentProtocol, }, - Group: group, - GroupIndex: i, + Group: group, + GroupIndex: i, + MinTealVersion: minTealVersion, } ctxs[i] = ctx } @@ -282,10 +285,11 @@ func LogicSig(txn *transactions.SignedTxn, ctx *Context) error { } ep := logic.EvalParams{ - Txn: txn, - Proto: &proto, - TxnGroup: ctx.Group, - GroupIndex: ctx.GroupIndex, + Txn: txn, + Proto: &proto, + TxnGroup: ctx.Group, + GroupIndex: ctx.GroupIndex, + MinTealVersion: &ctx.MinTealVersion, } pass, err := logic.Eval(txn.Lsig.Logic, ep) if err != nil { diff --git a/ledger/eval.go b/ledger/eval.go index 5ae032d67e..d898b9951a 100644 --- a/ledger/eval.go +++ b/ledger/eval.go @@ -463,6 +463,7 @@ func (eval *BlockEvaluator) TransactionGroup(txads []transactions.SignedTxnWithA // transaction in the group func (eval *BlockEvaluator) prepareAppEvaluators(txgroup []transactions.SignedTxnWithAD) (res []*appTealEvaluator) { var groupNoAD []transactions.SignedTxn + var minTealVersion uint64 res = make([]*appTealEvaluator, len(txgroup)) for i, txn := range txgroup { // Ignore any non-ApplicationCall transactions @@ -476,16 +477,18 @@ func (eval *BlockEvaluator) prepareAppEvaluators(txgroup []transactions.SignedTx for j := range txgroup { groupNoAD[j] = txgroup[j].SignedTxn } + minTealVersion = logic.ComputeMinTealVersion(groupNoAD) } // Construct an appTealEvaluator (implements // apply.StateEvaluator) for use in ApplicationCall transactions. steva := appTealEvaluator{ evalParams: logic.EvalParams{ - Txn: &groupNoAD[i], - Proto: &eval.proto, - TxnGroup: groupNoAD, - GroupIndex: i, + Txn: &groupNoAD[i], + Proto: &eval.proto, + TxnGroup: groupNoAD, + GroupIndex: i, + MinTealVersion: &minTealVersion, }, AppTealGlobals: AppTealGlobals{ CurrentRound: eval.prevHeader.Round + 1, From b5080899281d795fe4aab241eb9ec3cbd124fb0e Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 14:31:00 -0400 Subject: [PATCH 217/267] improve TestApplicationsDisallowOldTeal --- data/transactions/logic/eval_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index b00e3c448c..d768400e1a 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3658,6 +3658,7 @@ func TestApplicationsDisallowOldTeal(t *testing.T) { txn := makeSampleTxn() txn.Txn.Type = protocol.ApplicationCallTx + txn.Txn.RekeyTo = basics.Address{} txngroup := []transactions.SignedTxn{txn} ep.TxnGroup = txngroup From b5fe6ff3377e5abd6db8bcde0ee39d8221c25d4b Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 14:33:19 -0400 Subject: [PATCH 218/267] remove redundant check in test --- data/transactions/logic/eval_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index d768400e1a..506576f7cc 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3749,15 +3749,14 @@ func TestAllowedOpcodesV2(t *testing.T) { strings.Contains(err.Error(), "illegal opcode") || strings.Contains(err.Error(), "pc did not advance"), ) - _, err = Check(program, ep) - require.Error(t, err, source) - require.True(t, - strings.Contains(err.Error(), "illegal opcode") || - strings.Contains(err.Error(), "pc did not advance"), - ) _, err = Eval(program, ep) require.Error(t, err, source) require.Contains(t, err.Error(), "illegal opcode") + + // It is not necessary to test CheckStateful and EvalStateful + // here, because we separately test that CheckStateful and + // EvalStateful will always fail if called on a version < + // appsEnabledVersion } cnt++ } From 6fd42cdcbf8cb2a2619cae9518f946433361553c Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 14:40:52 -0400 Subject: [PATCH 219/267] improve TestBackwardCompatTxnFields modifications --- data/transactions/logic/backwardCompat_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index c00632941f..c57abab8d2 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -415,6 +415,10 @@ func TestBackwardCompatTxnFields(t *testing.T) { ledger := makeTestLedger(nil) txn := makeSampleTxn() + // We'll reject too early if we have a nonzero RekeyTo, because that + // field must be zero for every txn in the group if this is an old + // TEAL version + txn.Txn.RekeyTo = basics.Address{} txgroup := makeSampleTxnGroup(txn) for _, fs := range fields { field := fs.field.String() @@ -457,9 +461,6 @@ func TestBackwardCompatTxnFields(t *testing.T) { ep.Ledger = ledger ep.TxnGroup = txgroup - var minTealVersion uint64 - ep.MinTealVersion = &minTealVersion - // check failure with version check _, err = Eval(program, ep) require.Error(t, err) From e307d19e5f80ed798608e1f752912d1b398dd2cc Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 15:07:14 -0400 Subject: [PATCH 220/267] add TestAnyRekeyToOrApplicationRaisesMinTealVersion --- data/transactions/logic/eval_test.go | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 506576f7cc..397069f722 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3685,6 +3685,97 @@ func TestApplicationsDisallowOldTeal(t *testing.T) { require.NoError(t, err) } +func TestAnyRekeyToOrApplicationRaisesMinTealVersion(t *testing.T) { + const source = "int 1" + + // Construct a group of two payments, no rekeying + txn0 := makeSampleTxn() + txn0.Txn.Type = protocol.PaymentTx + txn0.Txn.RekeyTo = basics.Address{} + txn1 := txn0 + txngroup0 := []transactions.SignedTxn{txn0, txn1} + + // Construct a group of one payment, one ApplicationCall, no rekeying + txn2 := makeSampleTxn() + txn2.Txn.Type = protocol.PaymentTx + txn2.Txn.RekeyTo = basics.Address{} + txn3 := txn2 + txn3.Txn.Type = protocol.ApplicationCallTx + txngroup1 := []transactions.SignedTxn{txn2, txn3} + + // Construct a group of one payment, one rekeying payment + txn4 := makeSampleTxn() + txn4.Txn.Type = protocol.PaymentTx + txn5 := txn4 + txn4.Txn.RekeyTo = basics.Address{} + txn5.Txn.RekeyTo = basics.Address{1} + txngroup2 := []transactions.SignedTxn{txn4, txn5} + + type testcase struct { + group []transactions.SignedTxn + validFromVersion uint64 + } + + cases := []testcase{ + testcase{txngroup0, 0}, + testcase{txngroup1, appsEnabledVersion}, + testcase{txngroup2, rekeyingEnabledVersion}, + } + + for ci, cse := range cases { + t.Run(fmt.Sprintf("ci=%d", ci), func(t *testing.T) { + ep := defaultEvalParams(nil, nil) + ep.TxnGroup = cse.group + ep.Txn = &cse.group[0] + + // Computed MinTealVersion should be == validFromVersion + calc := ComputeMinTealVersion(cse.group) + require.Equal(t, calc, cse.validFromVersion) + + // Should fail for all versions < validFromVersion + expected := fmt.Sprintf("program version must be >= %d", cse.validFromVersion) + for v := uint64(0); v < cse.validFromVersion; v++ { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + + _, err = CheckStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), expected) + + _, _, err = EvalStateful(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), expected) + + _, err = Check(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), expected) + + _, err = Eval(program, ep) + require.Error(t, err) + require.Contains(t, err.Error(), expected) + } + + // Should succeed for all versions >= validFromVersionn + for v := cse.validFromVersion; v <= AssemblerMaxVersion; v++ { + program, err := AssembleStringWithVersion(source, v) + require.NoError(t, err) + + _, err = CheckStateful(program, ep) + require.NoError(t, err) + + _, _, err = EvalStateful(program, ep) + require.NoError(t, err) + + _, err = Check(program, ep) + require.NoError(t, err) + + _, err = Eval(program, ep) + require.NoError(t, err) + } + }) + } +} + // check all v2 opcodes: allowed in v2 and not allowed in v1 and v0 func TestAllowedOpcodesV2(t *testing.T) { t.Parallel() From 2d57d5a189f9a29c9c51e8a91f6a7a2746badc27 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 15:21:40 -0400 Subject: [PATCH 221/267] add e2e test for creating application with v1 stateful TEAL --- test/scripts/e2e_subs/e2e-app-simple.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/scripts/e2e_subs/e2e-app-simple.sh b/test/scripts/e2e_subs/e2e-app-simple.sh index 4243324cdf..047b68110c 100755 --- a/test/scripts/e2e_subs/e2e-app-simple.sh +++ b/test/scripts/e2e_subs/e2e-app-simple.sh @@ -14,9 +14,29 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') GLOBAL_INTS=2 +# Version 2 approval program echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" PROGRAM=($(${gcmd} clerk compile "${TEMPDIR}/simple.teal")) +# Version 1 approval program +echo -e 'int 1' > "${TEMPDIR}/simplev1.teal" + +# Fail in creating app with v1 approval program +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${TEMPDIR}/simplev1.teal" --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints ${GLOBAL_INTS} --local-byteslices 0 --local-ints 0 2>&1 || true) +EXPERROR='program version must be >= 2 for this transaction group' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL should fail to create app with v1 approval program %Y%m%d_%H%M%S' + false +fi + +# Fail in creating app with v1 clearstate program +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${TEMPDIR}/simple.teal" --clear-prog "${TEMPDIR}/simplev1.teal" --global-byteslices 0 --global-ints ${GLOBAL_INTS} --local-byteslices 0 --local-ints 0 2>&1 || true) +EXPERROR='program version must be >= 2 for this transaction group' +if [[ $RES != *"${EXPERROR}"* ]]; then + date '+app-create-test FAIL should fail to create app with v1 clearstate program %Y%m%d_%H%M%S' + false +fi + # Succeed in creating app that approves all transactions APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${TEMPDIR}/simple.teal" --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints ${GLOBAL_INTS} --local-byteslices 0 --local-ints 0 | grep Created | awk '{ print $6 }') @@ -47,7 +67,7 @@ if [[ ${PROGRAM[1]} != ${PROGRAM_CHECK[2]} ]]; then fi # Fail to create app if approval program rejects creation -RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 0') --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo -e '#pragma version 2\nint 0') --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) EXPERROR='rejected by ApprovalProgram' if [[ $RES != *"${EXPERROR}"* ]]; then date '+app-create-test FAIL txn with failing approval prog should be rejected %Y%m%d_%H%M%S' From 92420de91fe75dc149f82f7d1412da6a5c62b4e1 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 16:01:40 -0400 Subject: [PATCH 222/267] add e2e test for rekeying groups with v1 stateless TEAL --- test/scripts/e2e_subs/rekey.sh | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/scripts/e2e_subs/rekey.sh b/test/scripts/e2e_subs/rekey.sh index f070c14166..6f18909bbc 100755 --- a/test/scripts/e2e_subs/rekey.sh +++ b/test/scripts/e2e_subs/rekey.sh @@ -14,6 +14,60 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') +# Rekeying should fail if in a txn group with a < v2 TEAL program + +# Make v1 program +echo -e 'int 1' > "${TEMPDIR}/simplev1.teal" +ESCROWV1=$(${gcmd} clerk compile ${TEMPDIR}/simplev1.teal -o ${TEMPDIR}/simplev1.tealc | awk '{ print $2 }') + +# Make a > v1 program +echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" +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} + +# 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 + +# 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 + +# Broadcast group (should fail) +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' + false +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 + +# 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 + +# Broadcast group (should succeed) +${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: //') From 8bbc1d50784445a9d108b2082684c7a1b71f8981 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 16:15:22 -0400 Subject: [PATCH 223/267] add positive test for ForeignAsset index > 0 --- data/transactions/logic/evalStateful_test.go | 21 ++++++++++++++++++++ data/transactions/logic/eval_test.go | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 611d076acc..e2c511c028 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -1169,6 +1169,27 @@ int 1 require.NoError(t, err) require.True(t, pass) + source = `int 1 // foreign asset idx (txn.ForeignAssets[1]) +asset_params_get AssetURL +! +bnz error +len +int 9 +== +bnz ok +error: +err +ok: +int 1 +` + program, err = AssembleStringWithVersion(source, AssemblerMaxVersion) + require.NoError(t, err) + params.URL = "foobarbaz" + ledger.newAsset(77, params) + pass, _, err = EvalStateful(program, ep) + require.NoError(t, err) + require.True(t, pass) + source = `int 0 asset_params_get AssetURL ! diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 101e9e089e..36f220eeb0 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -1649,7 +1649,7 @@ func makeSampleTxn() transactions.SignedTxn { txn.Txn.FreezeAsset = 34 copy(txn.Txn.FreezeAccount[:], freezeAccAddr) txn.Txn.AssetFrozen = true - txn.Txn.ForeignAssets = []basics.AssetIndex{55} + txn.Txn.ForeignAssets = []basics.AssetIndex{55, 77} return txn } From 84e6170d64bbbd8243398a16ee76d33837c01188 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Thu, 30 Jul 2020 16:58:53 -0400 Subject: [PATCH 224/267] fix dryrun test --- daemon/algod/api/server/v2/test/handlers_test.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index f9537b6226..eead3fd01c 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -430,10 +430,10 @@ func TestTealDryrun(t *testing.T) { gdr.Txns = append(gdr.Txns, enc) } - sucProgram, err := logic.AssembleString("int 1") + sucProgram, err := logic.AssembleStringV2("int 1") require.NoError(t, err) - failProgram, err := logic.AssembleString("int 0") + failProgram, err := logic.AssembleStringV2("int 0") require.NoError(t, err) gdr.Apps = []generated.Application{ @@ -460,6 +460,7 @@ func TestTealDryrun(t *testing.T) { }, } + gdr.ProtocolVersion = string(protocol.ConsensusFuture) tealDryrunTest(t, &gdr, "json", 200, "PASS", true) tealDryrunTest(t, &gdr, "msgp", 200, "PASS", true) tealDryrunTest(t, &gdr, "msgp", 404, "", false) @@ -467,10 +468,14 @@ func TestTealDryrun(t *testing.T) { gdr.ProtocolVersion = "unk" tealDryrunTest(t, &gdr, "json", 400, "", true) gdr.ProtocolVersion = "" - ddr := tealDryrunTest(t, &gdr, "json", 200, "PASS", true) - require.Equal(t, string(protocol.ConsensusCurrentVersion), ddr.ProtocolVersion) + + // TODO(after applications) uncomment these two lines. The current + // protocol version does not support TEAL v2, which is required for + // application support. + // ddr := tealDryrunTest(t, &gdr, "json", 200, "PASS", true) + // require.Equal(t, string(protocol.ConsensusCurrentVersion), ddr.ProtocolVersion) gdr.ProtocolVersion = string(protocol.ConsensusFuture) - ddr = tealDryrunTest(t, &gdr, "json", 200, "PASS", true) + ddr := tealDryrunTest(t, &gdr, "json", 200, "PASS", true) require.Equal(t, string(protocol.ConsensusFuture), ddr.ProtocolVersion) gdr.Apps[0].Params.ApprovalProgram = failProgram From 15294a0415c457940f13aaeafa09707234ccda1c Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 30 Jul 2020 21:47:10 -0400 Subject: [PATCH 225/267] Add upgrade test for application over REST --- .../upgrades/application_support_test.go | 187 ++++++++++++++++++ .../upgrades/send_receive_upgrade_test.go | 2 +- 2 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 test/e2e-go/upgrades/application_support_test.go diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go new file mode 100644 index 0000000000..23649d947a --- /dev/null +++ b/test/e2e-go/upgrades/application_support_test.go @@ -0,0 +1,187 @@ +// 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 . + +package upgrades + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "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/protocol" + "github.com/algorand/go-algorand/test/framework/fixtures" +) + +// consensusTestUnupgradedProtocol is a version of ConsensusCurrentVersion +// that allows the control of the upgrade from consensusTestUnupgradedProtocol to +// consensusTestUnupgradedProtocol +const consensusTestUnupgradedProtocol = protocol.ConsensusVersion("test-unupgraded-protocol") + +func makeApplicationUpgradeConsensus(t *testing.T) (appConsensus config.ConsensusProtocols) { + appConsensus = generateFastUpgradeConsensus() + // make sure that the "current" version does not support application and that the "future" version *does* support applications. + currentProtocolParams, ok := appConsensus[consensusTestFastUpgrade(protocol.ConsensusCurrentVersion)] + require.True(t, ok) + futureProtocolParams, ok := appConsensus[consensusTestFastUpgrade(protocol.ConsensusFuture)] + require.True(t, ok) + + // ensure it's disabled. + currentProtocolParams.Application = false + + // verify that the future protocol supports applications. + require.True(t, futureProtocolParams.Application) + + // add an upgrade path from current to future. + currentProtocolParams.ApprovedUpgrades = make(map[protocol.ConsensusVersion]uint64) + currentProtocolParams.ApprovedUpgrades[consensusTestFastUpgrade(protocol.ConsensusFuture)] = 0 + + appConsensus[consensusTestUnupgradedProtocol] = currentProtocolParams + appConsensus[consensusTestFastUpgrade(protocol.ConsensusFuture)] = futureProtocolParams + + return +} + +// TestApplicationsUpgrade tests that we can safely upgrade from a version that doesn't support applications +// to a version that supports applications. It verify that prior to supporting applications, the node would not accept +// any application transaction and after the upgrade is complete, it would support that. +func TestApplicationsUpgradeOverREST(t *testing.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() + + secondaryClient := fixture.GetLibGoalClientForNamedNode("Node") + accountList, err := fixture.GetNodeWalletsSortedByBalance(secondaryClient.DataDir()) + require.NoError(t, err) + + creator := accountList[0].Address + wh, err := secondaryClient.GetUnencryptedWalletHandle() + require.NoError(t, err) + + user, err := secondaryClient.GenerateAddress(wh) + require.NoError(t, err) + + fee := uint64(1000) + + round, err := secondaryClient.CurrentRound() + require.NoError(t, err) + + // Fund the manager, so it can issue transactions later on + _, err = secondaryClient.SendPaymentFromUnencryptedWallet(creator, user, fee, 10000000000, nil) + require.NoError(t, err) + secondaryClient.WaitForRound(round + 2) + + // There should be no apps to start with + ad, err := secondaryClient.AccountData(creator) + require.NoError(t, err) + require.Zero(t, len(ad.AppParams)) + + ad, err = secondaryClient.AccountData(user) + require.NoError(t, err) + require.Zero(t, len(ad.AppParams)) + require.Equal(t, basics.MicroAlgos{Raw: 10000000000}, ad.MicroAlgos) + + counter := `#pragma version 2 +// a simple global and local calls counter app +byte b64 Y291bnRlcg== // counter +dup +app_global_get +int 1 ++ +app_global_put // update the counter +int 0 +int 0 +app_opted_in +bnz opted_in +err +opted_in: +int 0 // account idx for app_local_put +byte b64 Y291bnRlcg== // counter +int 0 +byte b64 Y291bnRlcg== +app_local_get +int 1 // increment ++ +app_local_put +int 1 +` + approval, err := logic.AssembleString(counter) + require.NoError(t, err) + clearstate, err := logic.AssembleString("int 1") + require.NoError(t, err) + schema := basics.StateSchema{ + NumUint: 1, + } + + // create the app + tx, err := secondaryClient.MakeUnsignedAppCreateTx( + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + ) + require.NoError(t, err) + tx, err = secondaryClient.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) + require.NoError(t, err) + signedTxn, err := secondaryClient.SignTransactionWithWallet(wh, nil, tx) + require.NoError(t, err) + round, err = secondaryClient.CurrentRound() + require.NoError(t, err) + + _, err = secondaryClient.BroadcastTransaction(signedTxn) + require.Error(t, err) + require.Contains(t, err.Error(), "application transaction not supported") + + curStatus, err := secondaryClient.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 = secondaryClient.Status() + require.NoError(t, err) + + require.Less(t, int64(time.Now().Sub(startLoopTime)), int64(3*time.Minute)) + time.Sleep(time.Duration(roundTimeMs) * time.Millisecond) + } + + // now, that we have upgraded to the new protocol which supports applications, try again. + _, err = secondaryClient.BroadcastTransaction(signedTxn) + require.NoError(t, err) + + return +} diff --git a/test/e2e-go/upgrades/send_receive_upgrade_test.go b/test/e2e-go/upgrades/send_receive_upgrade_test.go index ac65faf88a..2e573841ae 100644 --- a/test/e2e-go/upgrades/send_receive_upgrade_test.go +++ b/test/e2e-go/upgrades/send_receive_upgrade_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . -package transactions +package upgrades import ( "math/rand" From 61565b909ce7571f04de9fc1ee0b933698b0347b Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Thu, 30 Jul 2020 22:04:24 -0400 Subject: [PATCH 226/267] Add more complete testing. --- .../upgrades/application_support_test.go | 134 +++++++++++++++--- 1 file changed, 117 insertions(+), 17 deletions(-) diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go index 23649d947a..eb697e5f4f 100644 --- a/test/e2e-go/upgrades/application_support_test.go +++ b/test/e2e-go/upgrades/application_support_test.go @@ -85,33 +85,33 @@ func TestApplicationsUpgradeOverREST(t *testing.T) { fixture.Setup(t, filepath.Join("nettemplates", "TwoNodes100SecondTestUnupgradedProtocol.json")) defer fixture.Shutdown() - secondaryClient := fixture.GetLibGoalClientForNamedNode("Node") - accountList, err := fixture.GetNodeWalletsSortedByBalance(secondaryClient.DataDir()) + client := fixture.GetLibGoalClientForNamedNode("Node") + accountList, err := fixture.GetNodeWalletsSortedByBalance(client.DataDir()) require.NoError(t, err) creator := accountList[0].Address - wh, err := secondaryClient.GetUnencryptedWalletHandle() + wh, err := client.GetUnencryptedWalletHandle() require.NoError(t, err) - user, err := secondaryClient.GenerateAddress(wh) + user, err := client.GenerateAddress(wh) require.NoError(t, err) fee := uint64(1000) - round, err := secondaryClient.CurrentRound() + round, err := client.CurrentRound() require.NoError(t, err) // Fund the manager, so it can issue transactions later on - _, err = secondaryClient.SendPaymentFromUnencryptedWallet(creator, user, fee, 10000000000, nil) + _, err = client.SendPaymentFromUnencryptedWallet(creator, user, fee, 10000000000, nil) require.NoError(t, err) - secondaryClient.WaitForRound(round + 2) + client.WaitForRound(round + 2) // There should be no apps to start with - ad, err := secondaryClient.AccountData(creator) + ad, err := client.AccountData(creator) require.NoError(t, err) require.Zero(t, len(ad.AppParams)) - ad, err = secondaryClient.AccountData(user) + ad, err = client.AccountData(user) require.NoError(t, err) require.Zero(t, len(ad.AppParams)) require.Equal(t, basics.MicroAlgos{Raw: 10000000000}, ad.MicroAlgos) @@ -149,22 +149,22 @@ int 1 } // create the app - tx, err := secondaryClient.MakeUnsignedAppCreateTx( + tx, err := client.MakeUnsignedAppCreateTx( transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, ) require.NoError(t, err) - tx, err = secondaryClient.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) + tx, err = client.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) require.NoError(t, err) - signedTxn, err := secondaryClient.SignTransactionWithWallet(wh, nil, tx) + signedTxn, err := client.SignTransactionWithWallet(wh, nil, tx) require.NoError(t, err) - round, err = secondaryClient.CurrentRound() + round, err = client.CurrentRound() require.NoError(t, err) - _, err = secondaryClient.BroadcastTransaction(signedTxn) + _, err = client.BroadcastTransaction(signedTxn) require.Error(t, err) require.Contains(t, err.Error(), "application transaction not supported") - curStatus, err := secondaryClient.Status() + curStatus, err := client.Status() require.NoError(t, err) initialStatus := curStatus @@ -172,16 +172,116 @@ int 1 // wait until the network upgrade : this can take a while. for curStatus.LastVersion == initialStatus.LastVersion { - curStatus, err = secondaryClient.Status() + 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 applications, try again. - _, err = secondaryClient.BroadcastTransaction(signedTxn) + _, err = client.BroadcastTransaction(signedTxn) require.NoError(t, err) + curStatus, err = client.Status() + require.NoError(t, err) + + round = curStatus.LastRound + + client.WaitForRound(round + 2) + pendingTx, err := client.GetPendingTransactions(1) + require.NoError(t, err) + require.Equal(t, uint64(0), pendingTx.TotalTxns) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + require.NoError(t, err) + require.Equal(t, 1, len(ad.AppParams)) + var appIdx basics.AppIndex + var params basics.AppParams + for i, p := range ad.AppParams { + appIdx = i + params = p + break + } + require.Equal(t, approval, params.ApprovalProgram) + require.Equal(t, clearstate, params.ClearStateProgram) + require.Equal(t, schema, params.LocalStateSchema) + require.Equal(t, schema, params.GlobalStateSchema) + require.Equal(t, 1, len(params.GlobalState)) + value, ok := params.GlobalState["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok := ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + // call the app + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + require.NoError(t, err) + tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) + require.NoError(t, err) + signedTxn, err = client.SignTransactionWithWallet(wh, nil, tx) + require.NoError(t, err) + round, err = client.CurrentRound() + require.NoError(t, err) + _, err = client.BroadcastTransaction(signedTxn) + require.NoError(t, err) + + client.WaitForRound(round + 2) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + require.NoError(t, err) + require.Equal(t, 1, len(ad.AppParams)) + params, ok = ad.AppParams[appIdx] + require.True(t, ok) + require.Equal(t, approval, params.ApprovalProgram) + require.Equal(t, clearstate, params.ClearStateProgram) + require.Equal(t, schema, params.LocalStateSchema) + require.Equal(t, schema, params.GlobalStateSchema) + require.Equal(t, 1, len(params.GlobalState)) + value, ok = params.GlobalState["counter"] + require.True(t, ok) + require.Equal(t, uint64(2), value.Uint) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, uint64(2), ad.TotalAppSchema.NumUint) + + // check user's balance record for the app entry and the state changes + ad, err = client.AccountData(user) + require.NoError(t, err) + require.Equal(t, 0, len(ad.AppParams)) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, basics.MicroAlgos{Raw: 10000000000 - fee}, ad.MicroAlgos) + + app, err := client.ApplicationInformation(uint64(appIdx)) + require.NoError(t, err) + require.Equal(t, uint64(appIdx), app.Id) + require.Equal(t, creator, app.Params.Creator) return } From 486b61fd208726d54f65c92ac633bd97d03c0f66 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 10:36:19 -0400 Subject: [PATCH 227/267] fix some e2e tests --- test/scripts/e2e_subs/e2e-app-closeout.sh | 2 +- test/scripts/e2e_subs/e2e-app-real-assets-round.sh | 2 +- test/scripts/e2e_subs/e2e-app-x-app-reads.sh | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/scripts/e2e_subs/e2e-app-closeout.sh b/test/scripts/e2e_subs/e2e-app-closeout.sh index c116c3a755..db17ca541b 100755 --- a/test/scripts/e2e_subs/e2e-app-closeout.sh +++ b/test/scripts/e2e_subs/e2e-app-closeout.sh @@ -21,7 +21,7 @@ ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') ${gcmd} clerk send -a 100000000 -f ${ACCOUNT} -t ${ACCOUNTB} # Create an application that uses some global/local state -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo 'int 1') --global-byteslices 1 --global-ints 1 --local-byteslices 1 --local-ints 1 --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo -e '#pragma version 2\nint 1') --global-byteslices 1 --global-ints 1 --local-byteslices 1 --local-ints 1 --clear-prog <(echo -e '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') # Should succeed to opt in ${gcmd} app optin --app-id $APPID --from $ACCOUNTB diff --git a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh index ac6841b135..0ae7142b4d 100755 --- a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh +++ b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh @@ -23,7 +23,7 @@ ASSET_ID=$(${gcmd} asset info --creator $ACCOUNT --asset bogo|grep 'Asset ID'|aw # Create app that reads asset balance and checks asset details and checks round ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo "int 1") | grep Created | awk '{ print $6 }') +APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e "#pragma version 2\nint 1") | grep Created | awk '{ print $6 }') # Create another account, fund it, send it some asset ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') diff --git a/test/scripts/e2e_subs/e2e-app-x-app-reads.sh b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh index 4c1eb1237a..e7e0b82a6d 100755 --- a/test/scripts/e2e_subs/e2e-app-x-app-reads.sh +++ b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh @@ -17,12 +17,12 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Create an app with global state "foo" = "xxx" -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globwrite.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:xxx" --clear-prog <(echo 'int 1') | grep Created | awk '{ print $6 }') +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globwrite.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:xxx" --clear-prog <(echo -e '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') # Creating an app that attempts to read APPID's global state without setting # foreignapps should fail EXPERR="invalid ForeignApps index" -RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') 2>&1 || true) if [[ $RES != *"$EXPERR"* ]]; then date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' false @@ -32,7 +32,7 @@ fi # "bar" should make it past the foreign-app check, but fail since # "xxx" != "bar" EXPERR="rejected by ApprovalProgram" -RES=$(${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') 2>&1 || true) if [[ $RES != *"$EXPERR"* ]]; then date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' false @@ -43,4 +43,4 @@ ${gcmd} app call --app-id $APPID --from $ACCOUNT --app-arg "str:bar" # Creating other app should now succeed with properly set foreignapps AARGS="{args: [{encoding: \"int\", value: \"$APPID\"}], foreignapps: [$APPID]}" -${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo 'int 1') +${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') From 03fea170f205ca2bc0948ce4621915111cfbe482 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 31 Jul 2020 11:00:59 -0400 Subject: [PATCH 228/267] Add TestApplicationsUpgradeOverGossip test --- .../upgrades/application_support_test.go | 263 ++++++++++++++++++ ...oNodes100SecondTestUnupgradedProtocol.json | 6 +- 2 files changed, 266 insertions(+), 3 deletions(-) diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go index eb697e5f4f..32ae8ed0ed 100644 --- a/test/e2e-go/upgrades/application_support_test.go +++ b/test/e2e-go/upgrades/application_support_test.go @@ -285,3 +285,266 @@ int 1 require.Equal(t, creator, app.Params.Creator) return } + +// TestApplicationsUpgrade tests that we can safely upgrade from a version that doesn't support applications +// to a version that supports applications. It verify that prior to supporting applications, the node would not accept +// any application transaction and after the upgrade is complete, it would support that. +func TestApplicationsUpgradeOverGossip(t *testing.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.SetupNoStart(t, filepath.Join("nettemplates", "TwoNodes100SecondTestUnupgradedProtocol.json")) + + // for the primary node, we want to have a different consensus which always enables applications. + primaryNodeUnupgradedProtocol := consensus[consensusTestFastUpgrade(protocol.ConsensusFuture)] + primaryNodeUnupgradedProtocol.ApprovedUpgrades = make(map[protocol.ConsensusVersion]uint64) + primaryNodeUnupgradedProtocol.ApprovedUpgrades[consensusTestFastUpgrade(protocol.ConsensusFuture)] = 0 + consensus[consensusTestUnupgradedProtocol] = primaryNodeUnupgradedProtocol + + client := fixture.GetLibGoalClientForNamedNode("Primary") + secondary := fixture.GetLibGoalClientForNamedNode("Node") + err := config.SaveConfigurableConsensus(client.DataDir(), consensus) + require.NoError(t, err) + + fixture.Start() + + defer fixture.Shutdown() + + accountList, err := fixture.GetNodeWalletsSortedByBalance(client.DataDir()) + require.NoError(t, err) + + creator := accountList[0].Address + wh, err := client.GetUnencryptedWalletHandle() + require.NoError(t, err) + + user, err := client.GenerateAddress(wh) + require.NoError(t, err) + + fee := uint64(1000) + + round, err := client.CurrentRound() + require.NoError(t, err) + + // Fund the manager, so it can issue transactions later on + _, err = client.SendPaymentFromUnencryptedWallet(creator, user, fee, 10000000000, nil) + require.NoError(t, err) + client.WaitForRound(round + 2) + + round, err = client.CurrentRound() + require.NoError(t, err) + + // There should be no apps to start with + ad, err := client.AccountData(creator) + require.NoError(t, err) + require.Zero(t, len(ad.AppParams)) + + ad, err = client.AccountData(user) + require.NoError(t, err) + require.Zero(t, len(ad.AppParams)) + require.Equal(t, basics.MicroAlgos{Raw: 10000000000}, ad.MicroAlgos) + + counter := `#pragma version 2 +// a simple global and local calls counter app +byte b64 Y291bnRlcg== // counter +dup +app_global_get +int 1 ++ +app_global_put // update the counter +int 0 +int 0 +app_opted_in +bnz opted_in +err +opted_in: +int 0 // account idx for app_local_put +byte b64 Y291bnRlcg== // counter +int 0 +byte b64 Y291bnRlcg== +app_local_get +int 1 // increment ++ +app_local_put +int 1 +` + approval, err := logic.AssembleString(counter) + require.NoError(t, err) + clearstate, err := logic.AssembleString("int 1") + require.NoError(t, err) + schema := basics.StateSchema{ + NumUint: 1, + } + + // create the app + tx, err := client.MakeUnsignedAppCreateTx( + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + ) + require.NoError(t, err) + tx, err = client.FillUnsignedTxTemplate(creator, round, round+primaryNodeUnupgradedProtocol.DefaultUpgradeWaitRounds, fee, tx) + require.NoError(t, err) + signedTxn, err := client.SignTransactionWithWallet(wh, nil, tx) + require.NoError(t, err) + round, err = client.CurrentRound() + require.NoError(t, err) + + _, err = client.BroadcastTransaction(signedTxn) + require.NoError(t, err) + + // this transaction is expect to reach the first node ( primary ), but to be rejected by the second node when transmitted over gossip. + client.WaitForRound(round + 2) + + // check that the primary node still has this transaction in it's transaction pool. + pendingTx, err := client.GetPendingTransactions(1) + require.NoError(t, err) + + round, err = client.CurrentRound() + require.NoError(t, err) + if round > round+primaryNodeUnupgradedProtocol.DefaultUpgradeWaitRounds { + t.Skip("Test platform is too slow for this test") + } + + require.Equal(t, uint64(1), pendingTx.TotalTxns) + + // check that the secondary node doesn't have that transaction in it's transaction pool. + pendingTx, err = secondary.GetPendingTransactions(1) + require.NoError(t, err) + require.Equal(t, uint64(0), pendingTx.TotalTxns) + + 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 applications, try again. + tx, err = client.FillUnsignedTxTemplate(creator, round, round+100, fee, tx) + require.NoError(t, err) + signedTxn, err = client.SignTransactionWithWallet(wh, nil, tx) + require.NoError(t, err) + _, err = client.BroadcastTransaction(signedTxn) + require.NoError(t, err) + + curStatus, err = client.Status() + require.NoError(t, err) + + round = curStatus.LastRound + + client.WaitForRound(round + 2) + pendingTx, err = client.GetPendingTransactions(1) + require.NoError(t, err) + require.Equal(t, uint64(0), pendingTx.TotalTxns) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + require.NoError(t, err) + require.Equal(t, 1, len(ad.AppParams)) + var appIdx basics.AppIndex + var params basics.AppParams + for i, p := range ad.AppParams { + appIdx = i + params = p + break + } + require.Equal(t, approval, params.ApprovalProgram) + require.Equal(t, clearstate, params.ClearStateProgram) + require.Equal(t, schema, params.LocalStateSchema) + require.Equal(t, schema, params.GlobalStateSchema) + require.Equal(t, 1, len(params.GlobalState)) + value, ok := params.GlobalState["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok := ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + // call the app + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + require.NoError(t, err) + tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) + require.NoError(t, err) + signedTxn, err = client.SignTransactionWithWallet(wh, nil, tx) + require.NoError(t, err) + round, err = client.CurrentRound() + require.NoError(t, err) + _, err = client.BroadcastTransaction(signedTxn) + require.NoError(t, err) + + client.WaitForRound(round + 2) + + // check creator's balance record for the app entry and the state changes + ad, err = client.AccountData(creator) + require.NoError(t, err) + require.Equal(t, 1, len(ad.AppParams)) + params, ok = ad.AppParams[appIdx] + require.True(t, ok) + require.Equal(t, approval, params.ApprovalProgram) + require.Equal(t, clearstate, params.ClearStateProgram) + require.Equal(t, schema, params.LocalStateSchema) + require.Equal(t, schema, params.GlobalStateSchema) + require.Equal(t, 1, len(params.GlobalState)) + value, ok = params.GlobalState["counter"] + require.True(t, ok) + require.Equal(t, uint64(2), value.Uint) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, uint64(2), ad.TotalAppSchema.NumUint) + + // check user's balance record for the app entry and the state changes + ad, err = client.AccountData(user) + require.NoError(t, err) + require.Equal(t, 0, len(ad.AppParams)) + + require.Equal(t, 1, len(ad.AppLocalStates)) + state, ok = ad.AppLocalStates[appIdx] + require.True(t, ok) + require.Equal(t, schema, state.Schema) + require.Equal(t, 1, len(state.KeyValue)) + value, ok = state.KeyValue["counter"] + require.True(t, ok) + require.Equal(t, uint64(1), value.Uint) + + require.Equal(t, basics.MicroAlgos{Raw: 10000000000 - fee}, ad.MicroAlgos) + + app, err := client.ApplicationInformation(uint64(appIdx)) + require.NoError(t, err) + require.Equal(t, uint64(appIdx), app.Id) + require.Equal(t, creator, app.Params.Creator) + return +} diff --git a/test/testdata/nettemplates/TwoNodes100SecondTestUnupgradedProtocol.json b/test/testdata/nettemplates/TwoNodes100SecondTestUnupgradedProtocol.json index 760ff54d32..6d52d96e6c 100644 --- a/test/testdata/nettemplates/TwoNodes100SecondTestUnupgradedProtocol.json +++ b/test/testdata/nettemplates/TwoNodes100SecondTestUnupgradedProtocol.json @@ -5,12 +5,12 @@ "Wallets": [ { "Name": "Wallet1", - "Stake": 0, - "Online": true + "Stake": 10, + "Online": false }, { "Name": "Wallet2", - "Stake": 100, + "Stake": 90, "Online": true } ] From eab8c108aca0bbdba53211fbff00ca61425cfa8d Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 12:32:12 -0400 Subject: [PATCH 229/267] fix expect test --- test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index f477e61659..f05767b091 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -78,7 +78,7 @@ if { [catch { puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" set TEAL_PROG_FILE "$TEST_ROOT_DIR/trivial.teal" - exec echo int 1 > $TEAL_PROG_FILE + exec echo -e '#pragma version 2\nint 1' > $TEAL_PROG_FILE # no format parameter set DRREQ_FILE_1 "$TEST_ROOT_DIR/app-create-drreq-1.json" From 6637d083cc18c3b577ee91e26865005d16450b48 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 12:43:15 -0400 Subject: [PATCH 230/267] add min teal version to LogicSigSanityCheck --- data/transactions/verify/txn.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 90e560c081..1c7dac2e8b 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -223,10 +223,11 @@ func LogicSigSanityCheck(txn *transactions.SignedTxn, ctx *Context) error { } ep := logic.EvalParams{ - Txn: txn, - Proto: &proto, - TxnGroup: ctx.Group, - GroupIndex: ctx.GroupIndex, + Txn: txn, + Proto: &proto, + TxnGroup: ctx.Group, + GroupIndex: ctx.GroupIndex, + MinTealVersion: &ctx.MinTealVersion, } cost, err := logic.Check(lsig.Logic, ep) if err != nil { From 460656ab7942e3cb8b3757f7ff6781093113a2d4 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 13:46:32 -0400 Subject: [PATCH 231/267] move MinTealVersion to context params --- data/transactions/verify/txn.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 1c7dac2e8b..47c2e30339 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -41,9 +41,8 @@ var logicErrTotal = metrics.MakeCounter(metrics.MetricName{Name: "algod_ledger_l // on a signed transaction. type Context struct { Params - Group []transactions.SignedTxn - GroupIndex int - MinTealVersion uint64 + Group []transactions.SignedTxn + GroupIndex int } // Params is the set of parameters external to a transaction which @@ -55,8 +54,9 @@ type Context struct { // Group data are omitted because they are committed to in the // transaction and its ID. type Params struct { - CurrSpecAddrs transactions.SpecialAddresses - CurrProto protocol.ConsensusVersion + CurrSpecAddrs transactions.SpecialAddresses + CurrProto protocol.ConsensusVersion + MinTealVersion uint64 } // PrepareContexts prepares verification contexts for a transaction @@ -71,12 +71,12 @@ func PrepareContexts(group []transactions.SignedTxn, contextHdr bookkeeping.Bloc } ctx := Context{ Params: Params{ - CurrSpecAddrs: spec, - CurrProto: contextHdr.CurrentProtocol, + CurrSpecAddrs: spec, + CurrProto: contextHdr.CurrentProtocol, + MinTealVersion: minTealVersion, }, Group: group, GroupIndex: i, - MinTealVersion: minTealVersion, } ctxs[i] = ctx } From 49aeb47acfc34c1722f7993f925065a5242608c5 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 13:51:14 -0400 Subject: [PATCH 232/267] make fmt --- config/consensus.go | 2 +- data/transactions/verify/txn.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/consensus.go b/config/consensus.go index e9892d85a4..01b75cba8e 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -495,7 +495,7 @@ func initConsensusProtocols() { MaxBalLookback: 320, - MaxTxGroupSize: 1, + MaxTxGroupSize: 1, } v7.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 47c2e30339..eb433a089d 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -75,8 +75,8 @@ func PrepareContexts(group []transactions.SignedTxn, contextHdr bookkeeping.Bloc CurrProto: contextHdr.CurrentProtocol, MinTealVersion: minTealVersion, }, - Group: group, - GroupIndex: i, + Group: group, + GroupIndex: i, } ctxs[i] = ctx } From a8c2b0e57f5ca871be3b6ca26a67e2c024ec5a77 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 13:58:25 -0400 Subject: [PATCH 233/267] change double quotes to single quotes --- test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index f05767b091..1f287e8cb8 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -78,7 +78,7 @@ if { [catch { puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" set TEAL_PROG_FILE "$TEST_ROOT_DIR/trivial.teal" - exec echo -e '#pragma version 2\nint 1' > $TEAL_PROG_FILE + exec echo -e "#pragma version 2\nint 1" > $TEAL_PROG_FILE # no format parameter set DRREQ_FILE_1 "$TEST_ROOT_DIR/app-create-drreq-1.json" From a7ba85b0dd48deca8eb0ff35d109f94626aa4bc5 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 14:45:50 -0400 Subject: [PATCH 234/267] revert TestAllowedOpcodesV2 changes --- data/transactions/logic/eval_test.go | 14 +++++++++----- data/transactions/logic/opcodes.go | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 397069f722..6f29c81be0 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3840,14 +3840,18 @@ func TestAllowedOpcodesV2(t *testing.T) { strings.Contains(err.Error(), "illegal opcode") || strings.Contains(err.Error(), "pc did not advance"), ) + _, err = CheckStateful(program, ep) + require.Error(t, err, source) + require.True(t, + strings.Contains(err.Error(), "illegal opcode") || + strings.Contains(err.Error(), "pc did not advance"), + ) _, err = Eval(program, ep) require.Error(t, err, source) require.Contains(t, err.Error(), "illegal opcode") - - // It is not necessary to test CheckStateful and EvalStateful - // here, because we separately test that CheckStateful and - // EvalStateful will always fail if called on a version < - // appsEnabledVersion + _, _, err = EvalStateful(program, ep) + require.Error(t, err, source) + require.Contains(t, err.Error(), "illegal opcode") } cnt++ } diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index a4dc0fe13d..406b327272 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -25,12 +25,12 @@ const LogicVersion = 2 // rekeyingEnabledVersion is the version of TEAL where RekeyTo functionality // was enabled. This is important to remember so that old TEAL accounts cannot -// be maliciously or accidentally rekeyed. +// be maliciously or accidentally rekeyed. Do not edit! const rekeyingEnabledVersion = 2 // appsEnabledVersion is the version of TEAL where ApplicationCall // functionality was enabled. We use this to disallow v0 and v1 TEAL programs -// from being used with applications. +// from being used with applications. Do not edit! const appsEnabledVersion = 2 // opSize records the length in bytes for an op that is constant-length but not length 1 From bfd72d67cf0aa71ca29dfe79b10850d9f4e2ab6c Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Fri, 31 Jul 2020 15:44:23 -0400 Subject: [PATCH 235/267] Update comments. --- test/e2e-go/features/catchup/basicCatchup_test.go | 2 +- test/e2e-go/upgrades/application_support_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e-go/features/catchup/basicCatchup_test.go b/test/e2e-go/features/catchup/basicCatchup_test.go index f61050e515..2c6e056b07 100644 --- a/test/e2e-go/features/catchup/basicCatchup_test.go +++ b/test/e2e-go/features/catchup/basicCatchup_test.go @@ -178,7 +178,7 @@ func runCatchupOverGossip(t *testing.T, // consensusTestUnupgradedProtocol is a version of ConsensusCurrentVersion // that allows the control of the upgrade from consensusTestUnupgradedProtocol to -// consensusTestUnupgradedProtocol +// consensusTestUnupgradedToProtocol const consensusTestUnupgradedProtocol = protocol.ConsensusVersion("test-unupgraded-protocol") // consensusTestUnupgradedToProtocol is a version of ConsensusCurrentVersion diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go index 32ae8ed0ed..5e924b2c9f 100644 --- a/test/e2e-go/upgrades/application_support_test.go +++ b/test/e2e-go/upgrades/application_support_test.go @@ -35,7 +35,7 @@ import ( // consensusTestUnupgradedProtocol is a version of ConsensusCurrentVersion // that allows the control of the upgrade from consensusTestUnupgradedProtocol to -// consensusTestUnupgradedProtocol +// test-fast-upgrade-future const consensusTestUnupgradedProtocol = protocol.ConsensusVersion("test-unupgraded-protocol") func makeApplicationUpgradeConsensus(t *testing.T) (appConsensus config.ConsensusProtocols) { From 41b881589bd248962cc56cd4e1a0ef247456be2a Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 15:59:41 -0400 Subject: [PATCH 236/267] add apps to goal account list --- cmd/goal/accountsList.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cmd/goal/accountsList.go b/cmd/goal/accountsList.go index e82e0a5ff0..4b124b1ab3 100644 --- a/cmd/goal/accountsList.go +++ b/cmd/goal/accountsList.go @@ -231,6 +231,21 @@ func (accountList *AccountsList) outputAccount(addr string, acctInfo v1.Account, } fmt.Printf("]") } + if len(acctInfo.AppParams) > 0 { + fmt.Printf("\t[created apps:") + for aid := range acctInfo.AppParams { + fmt.Printf(" %d", aid) + } + fmt.Printf("]") + } + if len(acctInfo.AppLocalStates) > 0 { + fmt.Printf("\t[opted in apps:") + for aid := range acctInfo.AppLocalStates { + fmt.Printf(" %d", aid) + } + fmt.Printf("]") + } + if accountList.isDefault(addr) { fmt.Printf("\t*Default") } From 7322751345518015aba15ef168a4a2327ecf3e24 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 17:25:29 -0400 Subject: [PATCH 237/267] ensure TEAL check shows the correct error message --- data/transactions/logic/eval.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 3553c3d1f5..4266f4fbc0 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -487,9 +487,12 @@ func check(program []byte, params EvalParams) (cost int, err error) { return } - for (cx.err == nil) && (cx.pc < len(cx.program)) { + for cx.pc < len(cx.program) { prevpc := cx.pc cost += cx.checkStep() + if cx.err != nil { + break + } if cx.pc <= prevpc { err = fmt.Errorf("pc did not advance, stuck at %d", cx.pc) return From f6e61aacb215b0221299e97423b4aa165996e5ac Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 17:34:07 -0400 Subject: [PATCH 238/267] fix accountv2 unit test --- test/e2e-go/features/transactions/accountv2_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e-go/features/transactions/accountv2_test.go b/test/e2e-go/features/transactions/accountv2_test.go index 695d43c3fe..8ec844c46c 100644 --- a/test/e2e-go/features/transactions/accountv2_test.go +++ b/test/e2e-go/features/transactions/accountv2_test.go @@ -104,7 +104,7 @@ int 1 ` approval, err := logic.AssembleString(counter) a.NoError(err) - clearstate, err := logic.AssembleString("int 1") + clearstate, err := logic.AssembleString("#pragma version 2\nint 1") a.NoError(err) schema := basics.StateSchema{ NumUint: 1, From cf4c36f200eac0fb547b516c4eb9be9bfeefe993 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Fri, 31 Jul 2020 17:36:13 -0400 Subject: [PATCH 239/267] remove unnecessary loose check in TestAllowedOpcodesV2 --- data/transactions/logic/eval_test.go | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 36f220eeb0..52768f0085 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3713,16 +3713,10 @@ func TestAllowedOpcodesV2(t *testing.T) { program[0] = v _, err = Check(program, ep) require.Error(t, err, source) - require.True(t, - strings.Contains(err.Error(), "illegal opcode") || - strings.Contains(err.Error(), "pc did not advance"), - ) + require.Contains(t, err.Error(), "illegal opcode") _, err = CheckStateful(program, ep) require.Error(t, err, source) - require.True(t, - strings.Contains(err.Error(), "illegal opcode") || - strings.Contains(err.Error(), "pc did not advance"), - ) + require.Contains(t, err.Error(), "illegal opcode") _, err = Eval(program, ep) require.Error(t, err, source) require.Contains(t, err.Error(), "illegal opcode") From df5d0d499f24202b814591157b53aaca957eab59 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Fri, 31 Jul 2020 18:17:40 -0400 Subject: [PATCH 240/267] Do not save loop variable addresses in conversion routines --- daemon/algod/api/server/v2/account.go | 68 +++++++--- daemon/algod/api/server/v2/account_test.go | 137 ++++++++++++++++----- tools/network/dnssec/testHarness.go | 3 +- 3 files changed, 156 insertions(+), 52 deletions(-) diff --git a/daemon/algod/api/server/v2/account.go b/daemon/algod/api/server/v2/account.go index 190e8a404e..d4d28a697c 100644 --- a/daemon/algod/api/server/v2/account.go +++ b/daemon/algod/api/server/v2/account.go @@ -183,33 +183,58 @@ func AccountToAccountData(a *generated.Account) (basics.AccountData, error) { var assetParams map[basics.AssetIndex]basics.AssetParams if a.CreatedAssets != nil && len(*a.CreatedAssets) > 0 { assetParams = make(map[basics.AssetIndex]basics.AssetParams, len(*a.CreatedAssets)) + var err error for _, ca := range *a.CreatedAssets { var metadataHash [32]byte - copy(metadataHash[:], *ca.Params.MetadataHash) - manager, err := basics.UnmarshalChecksumAddress(*ca.Params.Manager) - if err != nil { - return basics.AccountData{}, err + if ca.Params.MetadataHash != nil { + copy(metadataHash[:], *ca.Params.MetadataHash) } - reserve, err := basics.UnmarshalChecksumAddress(*ca.Params.Reserve) - if err != nil { - return basics.AccountData{}, err + var manager, reserve, freeze, clawback basics.Address + if ca.Params.Manager != nil { + if manager, err = basics.UnmarshalChecksumAddress(*ca.Params.Manager); err != nil { + return basics.AccountData{}, err + } } - freeze, err := basics.UnmarshalChecksumAddress(*ca.Params.Freeze) - if err != nil { - return basics.AccountData{}, err + if ca.Params.Reserve != nil { + if reserve, err = basics.UnmarshalChecksumAddress(*ca.Params.Reserve); err != nil { + return basics.AccountData{}, err + } } - clawback, err := basics.UnmarshalChecksumAddress(*ca.Params.Clawback) - if err != nil { - return basics.AccountData{}, err + if ca.Params.Freeze != nil { + if freeze, err = basics.UnmarshalChecksumAddress(*ca.Params.Freeze); err != nil { + return basics.AccountData{}, err + } + } + if ca.Params.Clawback != nil { + if clawback, err = basics.UnmarshalChecksumAddress(*ca.Params.Clawback); err != nil { + return basics.AccountData{}, err + } + } + + var defaultFrozen bool + if ca.Params.DefaultFrozen != nil { + defaultFrozen = *ca.Params.DefaultFrozen + } + var url string + if ca.Params.Url != nil { + url = *ca.Params.Url + } + var unitName string + if ca.Params.UnitName != nil { + unitName = *ca.Params.UnitName + } + var name string + if ca.Params.Name != nil { + name = *ca.Params.Name } assetParams[basics.AssetIndex(ca.Index)] = basics.AssetParams{ Total: ca.Params.Total, Decimals: uint32(ca.Params.Decimals), - DefaultFrozen: *ca.Params.DefaultFrozen, - UnitName: *ca.Params.UnitName, - AssetName: *ca.Params.Name, - URL: *ca.Params.Url, + DefaultFrozen: defaultFrozen, + UnitName: unitName, + AssetName: name, + URL: url, MetadataHash: metadataHash, Manager: manager, Reserve: reserve, @@ -360,12 +385,12 @@ func AppParamsToApplication(creator string, appIdx basics.AppIndex, appParams *b // AssetParamsToAsset converts basics.AssetParams to generated.Asset func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.AssetParams) generated.Asset { + frozen := params.DefaultFrozen assetParams := generated.AssetParams{ Creator: creator, Total: params.Total, Decimals: uint64(params.Decimals), - DefaultFrozen: ¶ms.DefaultFrozen, - MetadataHash: byteOrNil(params.MetadataHash[:]), + DefaultFrozen: &frozen, Name: strOrNil(params.AssetName), UnitName: strOrNil(params.UnitName), Url: strOrNil(params.URL), @@ -374,6 +399,11 @@ func AssetParamsToAsset(creator string, idx basics.AssetIndex, params *basics.As Manager: addrOrNil(params.Manager), Reserve: addrOrNil(params.Reserve), } + if params.MetadataHash != ([32]byte{}) { + metadataHash := make([]byte, len(params.MetadataHash)) + copy(metadataHash, params.MetadataHash[:]) + assetParams.MetadataHash = &metadataHash + } return generated.Asset{ Index: uint64(idx), diff --git a/daemon/algod/api/server/v2/account_test.go b/daemon/algod/api/server/v2/account_test.go index 38fab7f805..7cef29a8f9 100644 --- a/daemon/algod/api/server/v2/account_test.go +++ b/daemon/algod/api/server/v2/account_test.go @@ -17,6 +17,7 @@ package v2 import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -29,30 +30,58 @@ import ( func TestAccount(t *testing.T) { proto := config.Consensus[protocol.ConsensusFuture] - appIdx := basics.AppIndex(1) + appIdx1 := basics.AppIndex(1) + appIdx2 := basics.AppIndex(2) + assetIdx1 := basics.AssetIndex(3) + assetIdx2 := basics.AssetIndex(4) round := basics.Round(2) - params := basics.AppParams{ + appParams1 := basics.AppParams{ ApprovalProgram: []byte{1}, StateSchemas: basics.StateSchemas{ GlobalStateSchema: basics.StateSchema{NumUint: 1}, }, } + appParams2 := basics.AppParams{ + ApprovalProgram: []byte{2}, + StateSchemas: basics.StateSchemas{ + GlobalStateSchema: basics.StateSchema{NumUint: 2}, + }, + } + assetParams1 := basics.AssetParams{ + Total: 100, + DefaultFrozen: false, + UnitName: "unit1", + } + assetParams2 := basics.AssetParams{ + Total: 200, + DefaultFrozen: true, + UnitName: "unit2", + } + copy(assetParams2.MetadataHash[:], []byte("test2")) a := basics.AccountData{ Status: basics.Online, MicroAlgos: basics.MicroAlgos{Raw: 80000000}, RewardedMicroAlgos: basics.MicroAlgos{Raw: ^uint64(0)}, RewardsBase: 0, - AppParams: map[basics.AppIndex]basics.AppParams{appIdx: params}, + AppParams: map[basics.AppIndex]basics.AppParams{appIdx1: appParams1, appIdx2: appParams2}, AppLocalStates: map[basics.AppIndex]basics.AppLocalState{ - appIdx: { + appIdx1: { + Schema: basics.StateSchema{NumUint: 10}, + KeyValue: basics.TealKeyValue{ + "uint": basics.TealValue{Type: basics.TealUintType, Uint: 1}, + "bytes": basics.TealValue{Type: basics.TealBytesType, Bytes: "value1"}, + }, + }, + appIdx2: { Schema: basics.StateSchema{NumUint: 10}, KeyValue: basics.TealKeyValue{ "uint": basics.TealValue{Type: basics.TealUintType, Uint: 2}, - "bytes": basics.TealValue{Type: basics.TealBytesType, Bytes: "value"}, + "bytes": basics.TealValue{Type: basics.TealBytesType, Bytes: "value2"}, }, }, }, + AssetParams: map[basics.AssetIndex]basics.AssetParams{assetIdx1: assetParams1, assetIdx2: assetParams2}, } b := a.WithUpdatedRewards(proto, 100) @@ -62,37 +91,81 @@ func TestAccount(t *testing.T) { require.Equal(t, conv.Address, addr) require.Equal(t, conv.Amount, b.MicroAlgos.Raw) require.Equal(t, conv.AmountWithoutPendingRewards, a.MicroAlgos.Raw) + require.NotNil(t, conv.CreatedApps) - require.Equal(t, 1, len(*conv.CreatedApps)) - app := (*conv.CreatedApps)[0] - require.Equal(t, uint64(appIdx), app.Id) - require.Equal(t, params.ApprovalProgram, app.Params.ApprovalProgram) - require.Equal(t, params.GlobalStateSchema.NumUint, app.Params.GlobalStateSchema.NumUint) - require.Equal(t, params.GlobalStateSchema.NumByteSlice, app.Params.GlobalStateSchema.NumByteSlice) - require.NotNil(t, conv.AppsLocalState) - require.Equal(t, 1, len(*conv.AppsLocalState)) + require.Equal(t, 2, len(*conv.CreatedApps)) + for _, app := range *conv.CreatedApps { + var params basics.AppParams + if app.Id == uint64(appIdx1) { + params = appParams1 + } else if app.Id == uint64(appIdx2) { + params = appParams2 + } else { + require.Fail(t, fmt.Sprintf("app idx %d not in [%d, %d]", app.Id, appIdx1, appIdx2)) + } + require.Equal(t, params.ApprovalProgram, app.Params.ApprovalProgram) + require.Equal(t, params.GlobalStateSchema.NumUint, app.Params.GlobalStateSchema.NumUint) + require.Equal(t, params.GlobalStateSchema.NumByteSlice, app.Params.GlobalStateSchema.NumByteSlice) + } - ls := (*conv.AppsLocalState)[0] - require.Equal(t, uint64(appIdx), ls.Id) - require.Equal(t, uint64(10), ls.Schema.NumUint) - require.Equal(t, uint64(0), ls.Schema.NumByteSlice) - require.Equal(t, 2, len(*ls.KeyValue)) - value1 := generated.TealKeyValue{ - Key: b64("uint"), - Value: generated.TealValue{ - Type: uint64(basics.TealUintType), - Uint: 2, - }, + require.NotNil(t, conv.AppsLocalState) + require.Equal(t, 2, len(*conv.AppsLocalState)) + makeTKV := func(k string, v interface{}) generated.TealKeyValue { + value := generated.TealValue{} + switch v.(type) { + case int: + value.Uint = uint64(v.(int)) + value.Type = uint64(basics.TealUintType) + case string: + value.Bytes = b64(v.(string)) + value.Type = uint64(basics.TealBytesType) + default: + panic(fmt.Sprintf("Unknown teal type %v", t)) + } + return generated.TealKeyValue{ + Key: b64(k), + Value: value, + } } - value2 := generated.TealKeyValue{ - Key: b64("bytes"), - Value: generated.TealValue{ - Type: uint64(basics.TealBytesType), - Bytes: b64("value"), - }, + for _, ls := range *conv.AppsLocalState { + require.Equal(t, uint64(10), ls.Schema.NumUint) + require.Equal(t, uint64(0), ls.Schema.NumByteSlice) + require.Equal(t, 2, len(*ls.KeyValue)) + var value1 generated.TealKeyValue + var value2 generated.TealKeyValue + if ls.Id == uint64(appIdx1) { + value1 = makeTKV("uint", 1) + value2 = makeTKV("bytes", "value1") + } else if ls.Id == uint64(appIdx2) { + value1 = makeTKV("uint", 2) + value2 = makeTKV("bytes", "value2") + } else { + require.Fail(t, fmt.Sprintf("local state app idx %d not in [%d, %d]", ls.Id, appIdx1, appIdx2)) + } + require.Contains(t, *ls.KeyValue, value1) + require.Contains(t, *ls.KeyValue, value2) + } + + require.NotNil(t, conv.CreatedAssets) + require.Equal(t, 2, len(*conv.CreatedAssets)) + for _, asset := range *conv.CreatedAssets { + var params basics.AssetParams + if asset.Index == uint64(assetIdx1) { + params = assetParams1 + } else if asset.Index == uint64(assetIdx2) { + params = assetParams2 + } else { + require.Fail(t, fmt.Sprintf("asset idx %d not in [%d, %d]", asset.Index, assetIdx1, assetIdx2)) + } + require.Equal(t, params.Total, asset.Params.Total) + require.NotNil(t, asset.Params.DefaultFrozen) + require.Equal(t, params.DefaultFrozen, *asset.Params.DefaultFrozen) + require.NotNil(t, asset.Params.UnitName) + require.Equal(t, params.UnitName, *asset.Params.UnitName) + if asset.Params.MetadataHash != nil { + require.Equal(t, params.MetadataHash[:], *asset.Params.MetadataHash) + } } - require.Contains(t, *ls.KeyValue, value1) - require.Contains(t, *ls.KeyValue, value2) c, err := AccountToAccountData(&conv) require.NoError(t, err) diff --git a/tools/network/dnssec/testHarness.go b/tools/network/dnssec/testHarness.go index 0e6e1c5c8b..1c9839a989 100644 --- a/tools/network/dnssec/testHarness.go +++ b/tools/network/dnssec/testHarness.go @@ -193,7 +193,8 @@ func (r *testResolver) query(ctx context.Context, domain string, qtype uint16) ( msg.Answer = append(msg.Answer, rr) } for _, rr := range entry.sig { - msg.Answer = append(msg.Answer, &rr) + var rrsig dns.RRSIG = rr + msg.Answer = append(msg.Answer, &rrsig) } } } From f7a1cd1e6669701ee8d32e45fe3cee2ccaf674a3 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 1 Aug 2020 11:50:44 -0400 Subject: [PATCH 241/267] Update upgrade path. --- config/consensus.go | 61 +++++++++++-------- data/basics/teal_test.go | 1 + protocol/consensus.go | 5 ++ .../upgrades/send_receive_upgrade_test.go | 4 ++ .../TwoNodes50EachV23Upgrade.json | 35 +++++++++++ 5 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 test/testdata/nettemplates/TwoNodes50EachV23Upgrade.json diff --git a/config/consensus.go b/config/consensus.go index 14148c486e..a27455f25e 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -691,63 +691,72 @@ func initConsensusProtocols() { // v21 can be upgraded to v23. v21.ApprovedUpgrades[protocol.ConsensusV23] = 0 - // ConsensusFuture is used to test features that are implemented - // but not yet released in a production protocol version. - vFuture := v23 - vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} - vFuture.LogicSigVersion = 2 + // v24 is the stateful teal and rekeying upgrade + v24 := v23 + v24.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + v24.LogicSigVersion = 2 // Enable application support - vFuture.Application = true + v24.Application = true // Enable rekeying - vFuture.SupportRekeying = true + v24.SupportRekeying = true // 100.1 Algos (MinBalance for creating 1,000 assets) - vFuture.MaximumMinimumBalance = 100100000 + v24.MaximumMinimumBalance = 100100000 - vFuture.MaxAppArgs = 16 - vFuture.MaxAppTotalArgLen = 2048 - vFuture.MaxAppProgramLen = 1024 - vFuture.MaxAppKeyLen = 64 - vFuture.MaxAppBytesValueLen = 64 + v24.MaxAppArgs = 16 + v24.MaxAppTotalArgLen = 2048 + v24.MaxAppProgramLen = 1024 + v24.MaxAppKeyLen = 64 + v24.MaxAppBytesValueLen = 64 // 0.1 Algos (Same min balance cost as an Asset) - vFuture.AppFlatParamsMinBalance = 100000 - vFuture.AppFlatOptInMinBalance = 100000 + v24.AppFlatParamsMinBalance = 100000 + v24.AppFlatOptInMinBalance = 100000 // Can look up Sender + 4 other balance records per Application txn - vFuture.MaxAppTxnAccounts = 4 + v24.MaxAppTxnAccounts = 4 // Can look up 2 other app creator balance records to see global state - vFuture.MaxAppTxnForeignApps = 2 + v24.MaxAppTxnForeignApps = 2 // Can look up 2 assets to see asset parameters - vFuture.MaxAppTxnForeignAssets = 2 + v24.MaxAppTxnForeignAssets = 2 // 64 byte keys @ ~333 microAlgos/byte + delta - vFuture.SchemaMinBalancePerEntry = 25000 + v24.SchemaMinBalancePerEntry = 25000 // 9 bytes @ ~333 microAlgos/byte + delta - vFuture.SchemaUintMinBalance = 3500 + v24.SchemaUintMinBalance = 3500 // 64 byte values @ ~333 microAlgos/byte + delta - vFuture.SchemaBytesMinBalance = 25000 + v24.SchemaBytesMinBalance = 25000 // Maximum number of key/value pairs per local key/value store - vFuture.MaxLocalSchemaEntries = 16 + v24.MaxLocalSchemaEntries = 16 // Maximum number of key/value pairs per global key/value store - vFuture.MaxGlobalSchemaEntries = 64 + v24.MaxGlobalSchemaEntries = 64 // Maximum cost of ApprovalProgram/ClearStateProgram - vFuture.MaxAppProgramCost = 700 + v24.MaxAppProgramCost = 700 // Maximum number of apps a single account can create - vFuture.MaxAppsCreated = 10 + v24.MaxAppsCreated = 10 // Maximum number of apps a single account can opt into - vFuture.MaxAppsOptedIn = 10 + v24.MaxAppsOptedIn = 10 + Consensus[protocol.ConsensusV24] = v24 + + // v23 can be upgraded to v24. + v23.ApprovedUpgrades[protocol.ConsensusV24] = 0 + + // ConsensusFuture is used to test features that are implemented + // but not yet released in a production protocol version. + vFuture := v23 + vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + Consensus[protocol.ConsensusFuture] = vFuture } diff --git a/data/basics/teal_test.go b/data/basics/teal_test.go index e99db5b959..0de5553f5c 100644 --- a/data/basics/teal_test.go +++ b/data/basics/teal_test.go @@ -31,6 +31,7 @@ func TestStateDeltaValid(t *testing.T) { // test pre-applications proto protoPreF := config.Consensus[protocol.ConsensusV23] + a.False(protoPreF.Application) sd := StateDelta{"key": ValueDelta{Action: SetBytesAction, Bytes: "val"}} err := sd.Valid(&protoPreF) a.Error(err) diff --git a/protocol/consensus.go b/protocol/consensus.go index 86928db447..af89c112d9 100644 --- a/protocol/consensus.go +++ b/protocol/consensus.go @@ -128,6 +128,11 @@ const ConsensusV23 = ConsensusVersion( "https://github.com/algorandfoundation/specs/tree/e5f565421d720c6f75cdd186f7098495caf9101f", ) +// ConsensusV24 include the applications, rekeying and teal v2 +const ConsensusV24 = ConsensusVersion( + "https://github.com/algorandfoundation/specs/tree/3a83c4c743f8b17adfd73944b4319c25722a6782", +) + // 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( diff --git a/test/e2e-go/upgrades/send_receive_upgrade_test.go b/test/e2e-go/upgrades/send_receive_upgrade_test.go index ac65faf88a..54002b8c3a 100644 --- a/test/e2e-go/upgrades/send_receive_upgrade_test.go +++ b/test/e2e-go/upgrades/send_receive_upgrade_test.go @@ -87,6 +87,10 @@ func TestAccountsCanSendMoneyAcrossUpgradeV22toV23(t *testing.T) { testAccountsCanSendMoneyAcrossUpgrade(t, filepath.Join("nettemplates", "TwoNodes50EachV22Upgrade.json")) } +func TestAccountsCanSendMoneyAcrossUpgradeV23toV24(t *testing.T) { + testAccountsCanSendMoneyAcrossUpgrade(t, filepath.Join("nettemplates", "TwoNodes50EachV23Upgrade.json")) +} + // ConsensusTestFastUpgrade is meant for testing of protocol upgrades: // during testing, it is equivalent to another protocol with the exception // of the upgrade parameters, which allow for upgrades to take place after diff --git a/test/testdata/nettemplates/TwoNodes50EachV23Upgrade.json b/test/testdata/nettemplates/TwoNodes50EachV23Upgrade.json new file mode 100644 index 0000000000..51ff9f69fc --- /dev/null +++ b/test/testdata/nettemplates/TwoNodes50EachV23Upgrade.json @@ -0,0 +1,35 @@ +{ + "Genesis": { + "NetworkName": "tbd", + "ConsensusProtocol": "test-fast-upgrade-https://github.com/algorandfoundation/specs/tree/e5f565421d720c6f75cdd186f7098495caf9101f", + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 50, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 50, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [ + { "Name": "Wallet1", + "ParticipationOnly": false } + ] + }, + { + "Name": "Node", + "Wallets": [ + { "Name": "Wallet2", + "ParticipationOnly": false } + ] + } + ] +} From 6b8f74474b33ef70ad2a570f37aae5e415fcd5aa Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 1 Aug 2020 11:57:25 -0400 Subject: [PATCH 242/267] bugfix. --- config/consensus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/consensus.go b/config/consensus.go index a27455f25e..783424405f 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -754,7 +754,7 @@ func initConsensusProtocols() { // ConsensusFuture is used to test features that are implemented // but not yet released in a production protocol version. - vFuture := v23 + vFuture := v24 vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} Consensus[protocol.ConsensusFuture] = vFuture From ffcb5225615b123db83443e13d0df726972f3515 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 1 Aug 2020 14:57:35 -0400 Subject: [PATCH 243/267] fix default version. --- protocol/consensus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocol/consensus.go b/protocol/consensus.go index af89c112d9..adfd825428 100644 --- a/protocol/consensus.go +++ b/protocol/consensus.go @@ -145,7 +145,7 @@ const ConsensusFuture = ConsensusVersion( // ConsensusCurrentVersion is the latest version and should be used // when a specific version is not provided. -const ConsensusCurrentVersion = ConsensusV23 +const ConsensusCurrentVersion = ConsensusV24 // Error is used to indicate that an unsupported protocol has been detected. type Error ConsensusVersion From 4b815be64ce8349025242b7936ef46c6bb8f0abe Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sat, 1 Aug 2020 15:02:05 -0400 Subject: [PATCH 244/267] update unit tests. --- cmd/tealdbg/debugger_test.go | 1 + data/transactions/logic/backwardCompat_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cmd/tealdbg/debugger_test.go b/cmd/tealdbg/debugger_test.go index 2bbb80e9a8..e2501d2fc1 100644 --- a/cmd/tealdbg/debugger_test.go +++ b/cmd/tealdbg/debugger_test.go @@ -90,6 +90,7 @@ func (d *testDbgAdapter) eventLoop() { func TestDebuggerSimple(t *testing.T) { proto := config.Consensus[protocol.ConsensusV23] + require.False(t, proto.Application) debugger := MakeDebugger() da := makeTestDbgAdapter(t) diff --git a/data/transactions/logic/backwardCompat_test.go b/data/transactions/logic/backwardCompat_test.go index d745dd746e..735e0a26e1 100644 --- a/data/transactions/logic/backwardCompat_test.go +++ b/data/transactions/logic/backwardCompat_test.go @@ -363,6 +363,7 @@ func TestBackwardCompatGlobalFields(t *testing.T) { require.NoError(t, err) proto := config.Consensus[protocol.ConsensusV23] + require.False(t, proto.Application) ep := defaultEvalParams(nil, nil) ep.Proto = &proto ep.Ledger = ledger @@ -450,6 +451,7 @@ func TestBackwardCompatTxnFields(t *testing.T) { } proto := config.Consensus[protocol.ConsensusV23] + require.False(t, proto.Application) ep := defaultEvalParams(nil, nil) ep.Proto = &proto ep.Ledger = ledger From 2fb4e416d0022854ab803a7e4cc0e4cf1752d3cc Mon Sep 17 00:00:00 2001 From: Benjamin Toll Date: Sat, 1 Aug 2020 15:21:09 -0400 Subject: [PATCH 245/267] Added documentation for test env vars for running the expect tests --- test/e2e-go/cli/goal/expect/README.md | 56 +++++++++++++++++++++------ 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/test/e2e-go/cli/goal/expect/README.md b/test/e2e-go/cli/goal/expect/README.md index 1f7194640a..a4b47b9bdc 100644 --- a/test/e2e-go/cli/goal/expect/README.md +++ b/test/e2e-go/cli/goal/expect/README.md @@ -1,12 +1,12 @@ # Goal Testing with Expect -Expect is a framework for testing command line interfaces (CLI). It is a extension to the TCL shell designed to automate invoking, interacting with, and validating results of CLIs. - +Expect is a framework for testing command line interfaces (CLI). It is a extension to the TCL shell designed to automate invoking, interacting with, and validating results of CLIs. + We use expect to test the Algorand Goal CLI. -## Setup +## Setup -From the go-algorand root directory, setup the environment and build the binaries as described in the top-level project README.md file. +From the go-algorand root directory, setup the environment and build the binaries as described in the top-level project README.md file. #### Initialize the project ```bash @@ -29,7 +29,7 @@ make integration #### Set environment variables -The `GOPATH` should be set to your local Go projects directory. +The `GOPATH` should be set to your local Go projects directory. The `PATH` environment variable should include `$GOPATH/bin`. For example: ```bash @@ -37,19 +37,53 @@ export GOPATH=~/path/to/goprojects export PATH=$(go env GOPATH | cut -d':' -f1 ):${PATH} ``` - ## Running the Expect Tests +There are three (optional) environment variables that can be used to control the behavior of the tests: + +- TESTDATADIR + - The location of the `genesis.json` file. + - Defaults to `$GOPATH/src/github.com/algorand/go-algorand/test/testdata`. + +- TESTDIR + - This is the location to where test artifacts will be written. + - Defaults to a location in the `/tmp` directory tree that is created at runtime. + +- TESTFILTER + - Allows for fine-grained control over which tests are selected to be run. + - The filter is a regular expression. + - For example, if you had hundreds of tests and only wanted to test `barTest.exp` and `carTest.exp`, you'd + set the filter to be `export TESTFILTER=[b,c]ar`. + - Defaults to all tests (`.*`). + To run the Goal Expect test, run the following command from the top level go-algorand directory: -`go test -v test/e2e-go/cli/goal/expect/goal_expect_test.go` +``` +go test -v test/e2e-go/cli/goal/expect/goal_expect_test.go +``` +Here is an example of running the tests with a preset `TESTDIR` and `TESTFILTER`: + +``` +# This will target all tests such as `foobar1Test.exp`, `foobar2Test.exp`, etc. but not `foobar10Test.exp`. +export TESTFILTER=foobar[0-9]Test +export TESTDIR=baz + +go test -v test/e2e-go/cli/goal/expect/goal_expect_test.go + +# OR + +TESTFILTER=foobar[0-9]Test TESTDIR=baz go test -v test/e2e-go/cli/goal/expect/goal_expect_test.go +``` + +> Of course, a test can always be run directly by `expect`, i.e. `expect rekeyTest.exp $TESTDIR $TESTDATADIR`. ## Adding New Tests -To add a test, create a copy of the `test/e2e-go/cli/goal/expect/basicGoalTest.exp` file within the same directory. -Give it a name that reflects the purpose of the test, and make sure the the file name suffix matches `'Test.exp'`. This will allow it to be included when running the expect tests. - +To add a test, create a copy of the `test/e2e-go/cli/goal/expect/basicGoalTest.exp` file within the same directory. +Give it a name that reflects the purpose of the test, and make sure the file name suffix matches `'Test.exp'`. This will allow it to be included when running the expect tests. + ## Common Procedures -Reusable and commonly used goal commands can be defined as procedures. This helps reduce code bulk and errors in the expect tests. See the file `goalExpectCommon.exp` for the list of available procedures. +Reusable and commonly used goal commands can be defined as procedures. This helps reduce code bulk and errors in the expect tests. See the file `goalExpectCommon.exp` for the list of available procedures. + From 70d4cbaf53037da0c84fcf23ea718396203a82f6 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Sun, 2 Aug 2020 13:08:51 -0400 Subject: [PATCH 246/267] Fix merge issue. --- test/e2e-go/upgrades/application_support_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go index 5e924b2c9f..79643cfc8d 100644 --- a/test/e2e-go/upgrades/application_support_test.go +++ b/test/e2e-go/upgrades/application_support_test.go @@ -150,7 +150,7 @@ int 1 // create the app tx, err := client.MakeUnsignedAppCreateTx( - transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, nil, ) require.NoError(t, err) tx, err = client.FillUnsignedTxTemplate(creator, 0, 0, fee, tx) @@ -224,7 +224,7 @@ int 1 require.Equal(t, uint64(1), value.Uint) // call the app - tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil, nil) require.NoError(t, err) tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) require.NoError(t, err) @@ -390,7 +390,7 @@ int 1 // create the app tx, err := client.MakeUnsignedAppCreateTx( - transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, + transactions.OptInOC, approval, clearstate, schema, schema, nil, nil, nil, nil, ) require.NoError(t, err) tx, err = client.FillUnsignedTxTemplate(creator, round, round+primaryNodeUnupgradedProtocol.DefaultUpgradeWaitRounds, fee, tx) @@ -487,7 +487,7 @@ int 1 require.Equal(t, uint64(1), value.Uint) // call the app - tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil) + tx, err = client.MakeUnsignedAppOptInTx(uint64(appIdx), nil, nil, nil, nil) require.NoError(t, err) tx, err = client.FillUnsignedTxTemplate(user, 0, 0, fee, tx) require.NoError(t, err) From 65bc54d0afcec1059cedd90058c244bdbf5754e0 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 3 Aug 2020 09:09:42 -0400 Subject: [PATCH 247/267] Refactor expect runners by creating a unified expect fixture. --- .../cli/algod/expect/algod_expect_test.go | 140 +-------------- .../cli/algoh/expect/algoh_expect_test.go | 138 +-------------- .../cli/goal/expect/goal_expect_test.go | 135 +-------------- test/framework/fixtures/expectFixture.go | 163 ++++++++++++++++++ 4 files changed, 175 insertions(+), 401 deletions(-) create mode 100644 test/framework/fixtures/expectFixture.go diff --git a/test/e2e-go/cli/algod/expect/algod_expect_test.go b/test/e2e-go/cli/algod/expect/algod_expect_test.go index 0ffa2548b5..76e64996eb 100644 --- a/test/e2e-go/cli/algod/expect/algod_expect_test.go +++ b/test/e2e-go/cli/algod/expect/algod_expect_test.go @@ -13,146 +13,16 @@ // // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . - package expect import ( - "bytes" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "strings" "testing" - "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/test/framework/fixtures" ) -type algodExpectFixture struct { - testDir string - testDataDir string - testDirTmp bool - t *testing.T - testFilter string -} - -func (f *algodExpectFixture) initialize(t *testing.T) (err error) { - f.t = t - f.testDir = os.Getenv("TESTDIR") - if f.testDir == "" { - f.testDir, _ = ioutil.TempDir("", "tmp") - f.testDir = filepath.Join(f.testDir, "expect") - err = os.MkdirAll(f.testDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", f.testDir, err) - return - } - f.testDirTmp = true - } - f.testDataDir = os.Getenv("TESTDATADIR") - if f.testDataDir == "" { - f.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") - } - - f.testFilter = os.Getenv("TESTFILTER") - if f.testFilter == "" { - f.testFilter = ".*" - } - return -} - -func (f *algodExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { - testName = strings.Replace(testName, ".exp", "", -1) - workingDir = filepath.Join(f.testDir, testName) - err = os.Mkdir(workingDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", workingDir, err) - return - } - algoDir = filepath.Join(workingDir, "algod") - err = os.Mkdir(algoDir, 0755) - if err != nil { - f.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) - return - } - return -} - -func (f *algodExpectFixture) removeTestDir(workingDir string) (err error) { - err = os.RemoveAll(workingDir) - if err != nil { - f.t.Errorf("error removing test dir %s, with error %v", workingDir, err) - return - } - return -} - -// TestAlgodWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algod/expect directory -func TestAlgodWithExpect(t *testing.T) { - var f algodExpectFixture - var execCommand = exec.Command - expectFiles := make(map[string]string) // map expect test to full file name. - err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - if strings.HasSuffix(info.Name(), "Test.exp") { - expectFiles[info.Name()] = path - } - return nil - }) - require.NoError(t, err) - err = f.initialize(t) - require.NoError(t, err) - - for testName := range expectFiles { - if match, _ := regexp.MatchString(f.testFilter, testName); match { - t.Run(testName, func(t *testing.T) { - workingDir, algoDir, err := f.getTestDir(testName) - require.NoError(t, err) - t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, f.testDataDir) - cmd := execCommand("expect", testName, algoDir, f.testDataDir) - var outBuf bytes.Buffer - cmd.Stdout = &outBuf - - // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor - // attaches goroutine reading that blocks on io.Copy from stderr. - // Cmd::CombinedOutput sets stderr to stdout and also blocks. - // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. - // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. - // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. - // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. - errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) - if err != nil { - t.Logf("failed opening stderr temp file: %s\n", err.Error()) - t.Fail() - } - defer errFile.Close() // Close might error but we Sync it before leaving the scope - cmd.Stderr = errFile - - err = cmd.Run() - if err != nil { - var stderr string - var ferr error - if ferr = errFile.Sync(); ferr == nil { - if _, ferr = errFile.Seek(0, 0); ferr == nil { - if info, ferr := errFile.Stat(); ferr == nil { - errData := make([]byte, info.Size()) - if _, ferr = errFile.Read(errData); ferr == nil { - stderr = string(errData) - } - } - } - } - if ferr != nil { - stderr = ferr.Error() - } - t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) - t.Fail() - } else { - // t.Logf("stdout: %s", string(outBuf.Bytes())) - f.removeTestDir(workingDir) - } - }) - } - } +// TesthWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algod/expect directory +func TestGoalWithExpect(t *testing.T) { + et := fixtures.MakeExpectTest(t) + et.Run() } diff --git a/test/e2e-go/cli/algoh/expect/algoh_expect_test.go b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go index adb17abbfe..eb3fd9c5e8 100644 --- a/test/e2e-go/cli/algoh/expect/algoh_expect_test.go +++ b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go @@ -13,146 +13,16 @@ // // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see . - package expect import ( - "bytes" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "strings" "testing" - "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/test/framework/fixtures" ) -type algohExpectFixture struct { - testDir string - testDataDir string - testDirTmp bool - t *testing.T - testFilter string -} - -func (f *algohExpectFixture) initialize(t *testing.T) (err error) { - f.t = t - f.testDir = os.Getenv("TESTDIR") - if f.testDir == "" { - f.testDir, _ = ioutil.TempDir("", "tmp") - f.testDir = filepath.Join(f.testDir, "expect") - err = os.MkdirAll(f.testDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", f.testDir, err) - return - } - f.testDirTmp = true - } - f.testDataDir = os.Getenv("TESTDATADIR") - if f.testDataDir == "" { - f.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") - } - - f.testFilter = os.Getenv("TESTFILTER") - if f.testFilter == "" { - f.testFilter = ".*" - } - return -} - -func (f *algohExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { - testName = strings.Replace(testName, ".exp", "", -1) - workingDir = filepath.Join(f.testDir, testName) - err = os.Mkdir(workingDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", workingDir, err) - return - } - algoDir = filepath.Join(workingDir, "algoh") - err = os.Mkdir(algoDir, 0755) - if err != nil { - f.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) - return - } - return -} - -func (f *algohExpectFixture) removeTestDir(workingDir string) (err error) { - err = os.RemoveAll(workingDir) - if err != nil { - f.t.Errorf("error removing test dir %s, with error %v", workingDir, err) - return - } - return -} - // TesthWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algoh/expect directory -func TestAlgohWithExpect(t *testing.T) { - var f algohExpectFixture - var execCommand = exec.Command - expectFiles := make(map[string]string) // map expect test to full file name. - err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - if strings.HasSuffix(info.Name(), "Test.exp") { - expectFiles[info.Name()] = path - } - return nil - }) - require.NoError(t, err) - err = f.initialize(t) - require.NoError(t, err) - - for testName := range expectFiles { - if match, _ := regexp.MatchString(f.testFilter, testName); match { - t.Run(testName, func(t *testing.T) { - workingDir, algoDir, err := f.getTestDir(testName) - require.NoError(t, err) - t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, f.testDataDir) - cmd := execCommand("expect", testName, algoDir, f.testDataDir) - var outBuf bytes.Buffer - cmd.Stdout = &outBuf - - // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor - // attaches goroutine reading that blocks on io.Copy from stderr. - // Cmd::CombinedOutput sets stderr to stdout and also blocks. - // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. - // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. - // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. - // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. - errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) - if err != nil { - t.Logf("failed opening stderr temp file: %s\n", err.Error()) - t.Fail() - } - defer errFile.Close() // Close might error but we Sync it before leaving the scope - cmd.Stderr = errFile - - err = cmd.Run() - if err != nil { - var stderr string - var ferr error - if ferr = errFile.Sync(); ferr == nil { - if _, ferr = errFile.Seek(0, 0); ferr == nil { - if info, ferr := errFile.Stat(); ferr == nil { - errData := make([]byte, info.Size()) - if _, ferr = errFile.Read(errData); ferr == nil { - stderr = string(errData) - } - } - } - } - if ferr != nil { - stderr = ferr.Error() - } - t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) - t.Fail() - } else { - // t.Logf("stdout: %s", string(outBuf.Bytes())) - f.removeTestDir(workingDir) - } - }) - } - } +func TestGoalWithExpect(t *testing.T) { + et := fixtures.MakeExpectTest(t) + et.Run() } diff --git a/test/e2e-go/cli/goal/expect/goal_expect_test.go b/test/e2e-go/cli/goal/expect/goal_expect_test.go index 70205c1f39..11595d236a 100644 --- a/test/e2e-go/cli/goal/expect/goal_expect_test.go +++ b/test/e2e-go/cli/goal/expect/goal_expect_test.go @@ -17,142 +17,13 @@ package expect import ( - "bytes" - "io/ioutil" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "strings" "testing" - "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/test/framework/fixtures" ) -type goalExpectFixture struct { - testDir string - testDataDir string - testDirTmp bool - t *testing.T - testFilter string -} - -func (f *goalExpectFixture) initialize(t *testing.T) (err error) { - f.t = t - f.testDir = os.Getenv("TESTDIR") - if f.testDir == "" { - f.testDir, _ = ioutil.TempDir("", "tmp") - f.testDir = filepath.Join(f.testDir, "expect") - err = os.MkdirAll(f.testDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", f.testDir, err) - return - } - f.testDirTmp = true - } - f.testDataDir = os.Getenv("TESTDATADIR") - if f.testDataDir == "" { - f.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") - } - - f.testFilter = os.Getenv("TESTFILTER") - if f.testFilter == "" { - f.testFilter = ".*" - } - return -} - -func (f *goalExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { - testName = strings.Replace(testName, ".exp", "", -1) - workingDir = filepath.Join(f.testDir, testName) - err = os.Mkdir(workingDir, 0755) - if err != nil { - f.t.Errorf("error creating test dir %s, with error %v", workingDir, err) - return - } - algoDir = filepath.Join(workingDir, "algod") - err = os.Mkdir(algoDir, 0755) - if err != nil { - f.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) - return - } - return -} - -func (f *goalExpectFixture) removeTestDir(workingDir string) (err error) { - err = os.RemoveAll(workingDir) - if err != nil { - f.t.Errorf("error removing test dir %s, with error %v", workingDir, err) - return - } - return -} - // TestGoalWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/goal/expect directory func TestGoalWithExpect(t *testing.T) { - var f goalExpectFixture - var execCommand = exec.Command - expectFiles := make(map[string]string) // map expect test to full file name. - err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { - if strings.HasSuffix(info.Name(), "Test.exp") { - expectFiles[info.Name()] = path - } - return nil - }) - require.NoError(t, err) - err = f.initialize(t) - require.NoError(t, err) - - for testName := range expectFiles { - if match, _ := regexp.MatchString(f.testFilter, testName); match { - t.Run(testName, func(t *testing.T) { - workingDir, algoDir, err := f.getTestDir(testName) - require.NoError(t, err) - t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, f.testDataDir) - cmd := execCommand("expect", testName, algoDir, f.testDataDir) - var outBuf bytes.Buffer - cmd.Stdout = &outBuf - - // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor - // attaches goroutine reading that blocks on io.Copy from stderr. - // Cmd::CombinedOutput sets stderr to stdout and also blocks. - // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. - // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. - // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. - // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. - errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) - if err != nil { - t.Logf("failed opening stderr temp file: %s\n", err.Error()) - t.Fail() - } - defer errFile.Close() // Close might error but we Sync it before leaving the scope - cmd.Stderr = errFile - - err = cmd.Run() - if err != nil { - var stderr string - var ferr error - if ferr = errFile.Sync(); ferr == nil { - if _, ferr = errFile.Seek(0, 0); ferr == nil { - if info, ferr := errFile.Stat(); ferr == nil { - errData := make([]byte, info.Size()) - if _, ferr = errFile.Read(errData); ferr == nil { - stderr = string(errData) - } - } - } - } - if ferr != nil { - stderr = ferr.Error() - } - t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) - t.Fail() - } else { - // t.Logf("stdout: %s", string(outBuf.Bytes())) - f.removeTestDir(workingDir) - } - }) - } - } + et := fixtures.MakeExpectTest(t) + et.Run() } diff --git a/test/framework/fixtures/expectFixture.go b/test/framework/fixtures/expectFixture.go new file mode 100644 index 0000000000..e0530afe68 --- /dev/null +++ b/test/framework/fixtures/expectFixture.go @@ -0,0 +1,163 @@ +// 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 . + +package fixtures + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// ExpectFixture is a wrapper for running expect tests +type ExpectFixture struct { + testDir string + testDataDir string + testDirTmp bool + t *testing.T + testFilter string + expectFiles map[string]string +} + +func (ef *ExpectFixture) initialize(t *testing.T) (err error) { + ef.t = t + ef.testDir = os.Getenv("TESTDIR") + if ef.testDir == "" { + ef.testDir, _ = ioutil.TempDir("", "tmp") + ef.testDir = filepath.Join(ef.testDir, "expect") + err = os.MkdirAll(ef.testDir, 0755) + if err != nil { + ef.t.Errorf("error creating test dir %s, with error %v", ef.testDir, err) + return + } + ef.testDirTmp = true + } + ef.testDataDir = os.Getenv("TESTDATADIR") + if ef.testDataDir == "" { + ef.testDataDir = os.ExpandEnv("${GOPATH}/src/github.com/algorand/go-algorand/test/testdata") + } + + ef.testFilter = os.Getenv("TESTFILTER") + if ef.testFilter == "" { + ef.testFilter = ".*" + } + return +} + +func (ef *ExpectFixture) getTestDir(testName string) (workingDir, algoDir string, err error) { + testName = strings.Replace(testName, ".exp", "", -1) + workingDir = filepath.Join(ef.testDir, testName) + err = os.Mkdir(workingDir, 0755) + if err != nil { + ef.t.Errorf("error creating test dir %s, with error %v", workingDir, err) + return + } + algoDir = filepath.Join(workingDir, "algod") + err = os.Mkdir(algoDir, 0755) + if err != nil { + ef.t.Errorf("error creating algo dir %s, with error %v", algoDir, err) + return + } + return +} + +func (ef *ExpectFixture) removeTestDir(workingDir string) (err error) { + err = os.RemoveAll(workingDir) + if err != nil { + ef.t.Errorf("error removing test dir %s, with error %v", workingDir, err) + return + } + return +} + +// MakeExpectTest creates an expect test fixture for the current directory +func MakeExpectTest(t *testing.T) *ExpectFixture { + ef := &ExpectFixture{} + ef.expectFiles = make(map[string]string) // map expect test to full file name. + err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(info.Name(), "Test.exp") { + ef.expectFiles[info.Name()] = path + } + return nil + }) + require.NoError(t, err) + err = ef.initialize(t) + require.NoError(t, err) + return ef +} + +// Run Process all expect script files with suffix Test.exp within the current directroy +func (ef *ExpectFixture) Run() { + for testName := range ef.expectFiles { + if match, _ := regexp.MatchString(ef.testFilter, testName); match { + ef.t.Run(testName, func(t *testing.T) { + workingDir, algoDir, err := ef.getTestDir(testName) + require.NoError(t, err) + t.Logf("algoDir: %s\ntestDataDir:%s\n", algoDir, ef.testDataDir) + cmd := exec.Command("expect", testName, algoDir, ef.testDataDir) + var outBuf bytes.Buffer + cmd.Stdout = &outBuf + + // Set stderr to be a file descriptor. In other way Go's exec.Cmd::writerDescriptor + // attaches goroutine reading that blocks on io.Copy from stderr. + // Cmd::CombinedOutput sets stderr to stdout and also blocks. + // Cmd::Start + Cmd::Wait with manual pipes redirection etc also blocks. + // Wrapping 'expect' with 'expect "$@" 2>&1' also blocks on stdout reading. + // Cmd::Output with Cmd::Stderr == nil works but stderr get lost. + // Using os.File as stderr does not trigger goroutine creation, instead exec.Cmd relies on os.File implementation. + errFile, err := os.OpenFile(path.Join(workingDir, "stderr.txt"), os.O_CREATE|os.O_RDWR, 0) + if err != nil { + t.Logf("failed opening stderr temp file: %s\n", err.Error()) + t.Fail() + } + defer errFile.Close() // Close might error but we Sync it before leaving the scope + cmd.Stderr = errFile + + err = cmd.Run() + if err != nil { + var stderr string + var ferr error + if ferr = errFile.Sync(); ferr == nil { + if _, ferr = errFile.Seek(0, 0); ferr == nil { + if info, ferr := errFile.Stat(); ferr == nil { + errData := make([]byte, info.Size()) + if _, ferr = errFile.Read(errData); ferr == nil { + stderr = string(errData) + } + } + } + } + if ferr != nil { + stderr = ferr.Error() + } + t.Logf("err running '%s': %s\nstdout: %s\nstderr: %s\n", testName, err, string(outBuf.Bytes()), stderr) + t.Fail() + } else { + // t.Logf("stdout: %s", string(outBuf.Bytes())) + ef.removeTestDir(workingDir) + } + }) + } + } +} From 54562c10da3802cb73ba27bd6cc0a918e4ddbedd Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 3 Aug 2020 09:34:30 -0400 Subject: [PATCH 248/267] Remove deprecated sudo from travis. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9363b6ada9..8ad739124e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ go: - "1.12" go_import_path: github.com/algorand/go-algorand language: go -sudo: required # Don't build tags if: tag IS blank From 73f756b26c13a36e674da1f206945d8776c76999 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 10:31:14 -0400 Subject: [PATCH 249/267] echo -e -> printf --- test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp | 2 +- test/scripts/e2e_subs/e2e-app-bootloader.sh | 2 +- test/scripts/e2e_subs/e2e-app-closeout.sh | 2 +- test/scripts/e2e_subs/e2e-app-real-assets-round.sh | 2 +- test/scripts/e2e_subs/e2e-app-simple.sh | 6 +++--- test/scripts/e2e_subs/e2e-app-stateful-global.sh | 2 +- test/scripts/e2e_subs/e2e-app-stateful-local.sh | 2 +- test/scripts/e2e_subs/e2e-app-x-app-reads.sh | 8 ++++---- test/scripts/e2e_subs/rekey.sh | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp index 1f287e8cb8..baaf242635 100644 --- a/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp +++ b/test/e2e-go/cli/goal/expect/goalDryrunRestTest.exp @@ -78,7 +78,7 @@ if { [catch { puts "Primary Account Balance: $PRIMARY_ACCOUNT_BALANCE" set TEAL_PROG_FILE "$TEST_ROOT_DIR/trivial.teal" - exec echo -e "#pragma version 2\nint 1" > $TEAL_PROG_FILE + exec printf "#pragma version 2\nint 1" > $TEAL_PROG_FILE # no format parameter set DRREQ_FILE_1 "$TEST_ROOT_DIR/app-create-drreq-1.json" diff --git a/test/scripts/e2e_subs/e2e-app-bootloader.sh b/test/scripts/e2e_subs/e2e-app-bootloader.sh index 0759f24db3..cb90d3bfb5 100755 --- a/test/scripts/e2e_subs/e2e-app-bootloader.sh +++ b/test/scripts/e2e_subs/e2e-app-bootloader.sh @@ -32,7 +32,7 @@ sed -i"" -e "s/TMPL_APPROV_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal sed -i"" -e "s/TMPL_CLEARSTATE_HASH/${TARGET_HASH}/g" ${TEMPDIR}/bootloader.teal # Create an app using filled-in bootloader template -echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +printf '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${TEMPDIR}/bootloader.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Calling app without args and wrong OnCompletion should fail diff --git a/test/scripts/e2e_subs/e2e-app-closeout.sh b/test/scripts/e2e_subs/e2e-app-closeout.sh index db17ca541b..b34300e997 100755 --- a/test/scripts/e2e_subs/e2e-app-closeout.sh +++ b/test/scripts/e2e_subs/e2e-app-closeout.sh @@ -21,7 +21,7 @@ ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') ${gcmd} clerk send -a 100000000 -f ${ACCOUNT} -t ${ACCOUNTB} # Create an application that uses some global/local state -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo -e '#pragma version 2\nint 1') --global-byteslices 1 --global-ints 1 --local-byteslices 1 --local-ints 1 --clear-prog <(echo -e '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(printf '#pragma version 2\nint 1') --global-byteslices 1 --global-ints 1 --local-byteslices 1 --local-ints 1 --clear-prog <(printf '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') # Should succeed to opt in ${gcmd} app optin --app-id $APPID --from $ACCOUNTB diff --git a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh index c923b7c46d..5fbf33b64a 100755 --- a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh +++ b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh @@ -23,7 +23,7 @@ ASSET_ID=$(${gcmd} asset info --creator $ACCOUNT --asset bogo|grep 'Asset ID'|aw # Create app that reads asset balance and checks asset details and checks round ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e "#pragma version 2\nint 1") | grep Created | awk '{ print $6 }') +APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf "#pragma version 2\nint 1") | grep Created | awk '{ print $6 }') # Create another account, fund it, send it some asset ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') diff --git a/test/scripts/e2e_subs/e2e-app-simple.sh b/test/scripts/e2e_subs/e2e-app-simple.sh index 047b68110c..c6b415eac7 100755 --- a/test/scripts/e2e_subs/e2e-app-simple.sh +++ b/test/scripts/e2e_subs/e2e-app-simple.sh @@ -15,11 +15,11 @@ ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') GLOBAL_INTS=2 # Version 2 approval program -echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" +printf '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" PROGRAM=($(${gcmd} clerk compile "${TEMPDIR}/simple.teal")) # Version 1 approval program -echo -e 'int 1' > "${TEMPDIR}/simplev1.teal" +printf 'int 1' > "${TEMPDIR}/simplev1.teal" # Fail in creating app with v1 approval program RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog "${TEMPDIR}/simplev1.teal" --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints ${GLOBAL_INTS} --local-byteslices 0 --local-ints 0 2>&1 || true) @@ -67,7 +67,7 @@ if [[ ${PROGRAM[1]} != ${PROGRAM_CHECK[2]} ]]; then fi # Fail to create app if approval program rejects creation -RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(echo -e '#pragma version 2\nint 0') --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog <(printf '#pragma version 2\nint 0') --clear-prog "${TEMPDIR}/simple.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 2>&1 || true) EXPERROR='rejected by ApprovalProgram' if [[ $RES != *"${EXPERROR}"* ]]; then date '+app-create-test FAIL txn with failing approval prog should be rejected %Y%m%d_%H%M%S' diff --git a/test/scripts/e2e_subs/e2e-app-stateful-global.sh b/test/scripts/e2e_subs/e2e-app-stateful-global.sh index 9248061c74..c04ee84679 100755 --- a/test/scripts/e2e_subs/e2e-app-stateful-global.sh +++ b/test/scripts/e2e_subs/e2e-app-stateful-global.sh @@ -17,7 +17,7 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Succeed in creating app that approves transactions with arg[0] == 'hello' -echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +printf '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globcheck.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:hello" --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Application call with no args should fail diff --git a/test/scripts/e2e_subs/e2e-app-stateful-local.sh b/test/scripts/e2e_subs/e2e-app-stateful-local.sh index a7a1099380..b2281e7c7c 100755 --- a/test/scripts/e2e_subs/e2e-app-stateful-local.sh +++ b/test/scripts/e2e_subs/e2e-app-stateful-local.sh @@ -17,7 +17,7 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Succeed in creating app that approves transactions with arg[0] == 'hello' -echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" +printf '#pragma version 2\nint 1' > "${TEMPDIR}/int1.teal" APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/loccheck.teal --global-byteslices 0 --global-ints 0 --local-byteslices 1 --local-ints 0 --app-arg "str:hello" --clear-prog "${TEMPDIR}/int1.teal" | grep Created | awk '{ print $6 }') # Application call with no args should fail diff --git a/test/scripts/e2e_subs/e2e-app-x-app-reads.sh b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh index e7e0b82a6d..31eec64803 100755 --- a/test/scripts/e2e_subs/e2e-app-x-app-reads.sh +++ b/test/scripts/e2e_subs/e2e-app-x-app-reads.sh @@ -17,12 +17,12 @@ gcmd="goal -w ${WALLET}" ACCOUNT=$(${gcmd} account list|awk '{ print $3 }') # Create an app with global state "foo" = "xxx" -APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globwrite.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:xxx" --clear-prog <(echo -e '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') +APPID=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/globwrite.teal --global-byteslices 1 --global-ints 0 --local-byteslices 0 --local-ints 0 --app-arg "str:xxx" --clear-prog <(printf '#pragma version 2\nint 1') | grep Created | awk '{ print $6 }') # Creating an app that attempts to read APPID's global state without setting # foreignapps should fail EXPERR="invalid ForeignApps index" -RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf '#pragma version 2\nint 1') 2>&1 || true) if [[ $RES != *"$EXPERR"* ]]; then date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' false @@ -32,7 +32,7 @@ fi # "bar" should make it past the foreign-app check, but fail since # "xxx" != "bar" EXPERR="rejected by ApprovalProgram" -RES=$(${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') 2>&1 || true) +RES=$(${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf '#pragma version 2\nint 1') 2>&1 || true) if [[ $RES != *"$EXPERR"* ]]; then date '+x-app-reads FAIL expected disallowed foreign global read to fail %Y%m%d_%H%M%S' false @@ -43,4 +43,4 @@ ${gcmd} app call --app-id $APPID --from $ACCOUNT --app-arg "str:bar" # Creating other app should now succeed with properly set foreignapps AARGS="{args: [{encoding: \"int\", value: \"$APPID\"}], foreignapps: [$APPID]}" -${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(echo -e '#pragma version 2\nint 1') +${gcmd} app create --creator ${ACCOUNT} --foreign-app $APPID --approval-prog ${DIR}/tealprogs/xappreads.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf '#pragma version 2\nint 1') diff --git a/test/scripts/e2e_subs/rekey.sh b/test/scripts/e2e_subs/rekey.sh index 6f18909bbc..24fd9494d3 100755 --- a/test/scripts/e2e_subs/rekey.sh +++ b/test/scripts/e2e_subs/rekey.sh @@ -17,11 +17,11 @@ ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') # Rekeying should fail if in a txn group with a < v2 TEAL program # Make v1 program -echo -e 'int 1' > "${TEMPDIR}/simplev1.teal" +printf 'int 1' > "${TEMPDIR}/simplev1.teal" ESCROWV1=$(${gcmd} clerk compile ${TEMPDIR}/simplev1.teal -o ${TEMPDIR}/simplev1.tealc | awk '{ print $2 }') # Make a > v1 program -echo -e '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" +printf '#pragma version 2\nint 1' > "${TEMPDIR}/simple.teal" ESCROWV2=$(${gcmd} clerk compile ${TEMPDIR}/simple.teal -o ${TEMPDIR}/simple.tealc | awk '{ print $2 }') # Fund v1 escrow, v2 escrow, and ACCOUNTD From 8331181f1ff1b03ad668f607b570b9e427f6b8cb Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 11:20:53 -0400 Subject: [PATCH 250/267] fix bad e2e test merge --- test/scripts/e2e_subs/e2e-app-real-assets-round.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh index 5fbf33b64a..6d2b8ed0ae 100755 --- a/test/scripts/e2e_subs/e2e-app-real-assets-round.sh +++ b/test/scripts/e2e_subs/e2e-app-real-assets-round.sh @@ -23,7 +23,7 @@ ASSET_ID=$(${gcmd} asset info --creator $ACCOUNT --asset bogo|grep 'Asset ID'|aw # Create app that reads asset balance and checks asset details and checks round ROUND=$(goal node status | grep 'Last committed' | awk '{ print $4 }') TIMESTAMP=$(goal ledger block --strict ${ROUND} | jq .block.ts) -APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf "#pragma version 2\nint 1") | grep Created | awk '{ print $6 }') +APP_ID=$(${gcmd} app create --creator ${ACCOUNT} --foreign-asset $ASSET_ID --app-arg "int:$ASSET_ID" --app-arg "int:1337" --app-arg "int:0" --app-arg "int:0" --app-arg "int:1337" --app-arg "str:bogo" --app-arg "int:$ROUND" --app-arg "int:$TIMESTAMP" --approval-prog ${DIR}/tealprogs/assetround.teal --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --clear-prog <(printf "#pragma version 2\nint 1") | grep Created | awk '{ print $6 }') # Create another account, fund it, send it some asset ACCOUNTB=$(${gcmd} account new|awk '{ print $6 }') From 2fd8978f9f6c214d6f4775a76fa5e680e77b850e Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 11:26:18 -0400 Subject: [PATCH 251/267] apps -> app IDs --- cmd/goal/accountsList.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/goal/accountsList.go b/cmd/goal/accountsList.go index 4b124b1ab3..4f92854f9b 100644 --- a/cmd/goal/accountsList.go +++ b/cmd/goal/accountsList.go @@ -232,14 +232,14 @@ func (accountList *AccountsList) outputAccount(addr string, acctInfo v1.Account, fmt.Printf("]") } if len(acctInfo.AppParams) > 0 { - fmt.Printf("\t[created apps:") + fmt.Printf("\t[created app IDs:") for aid := range acctInfo.AppParams { fmt.Printf(" %d", aid) } fmt.Printf("]") } if len(acctInfo.AppLocalStates) > 0 { - fmt.Printf("\t[opted in apps:") + fmt.Printf("\t[opted in app IDs:") for aid := range acctInfo.AppLocalStates { fmt.Printf(" %d", aid) } From b70aa13eeb61246416352590eb26de67c7187c37 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 11:33:47 -0400 Subject: [PATCH 252/267] make goal account list slightly prettier with commas --- cmd/goal/accountsList.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/cmd/goal/accountsList.go b/cmd/goal/accountsList.go index 4f92854f9b..e9d4630c68 100644 --- a/cmd/goal/accountsList.go +++ b/cmd/goal/accountsList.go @@ -23,6 +23,7 @@ import ( "os" "os/user" "path/filepath" + "strings" "github.com/algorand/go-algorand/daemon/algod/api/spec/v1" "github.com/algorand/go-algorand/data/basics" @@ -225,25 +226,28 @@ func (accountList *AccountsList) outputAccount(addr string, acctInfo v1.Account, fmt.Printf("\t[%d/%d multisig]", multisigInfo.Threshold, len(multisigInfo.PKs)) } if len(acctInfo.AssetParams) > 0 { - fmt.Printf("\t[created assets:") + fmt.Printf("\t[created asset IDs: ") + var out []string for curid, params := range acctInfo.AssetParams { - fmt.Printf(" %d (%d %s)", curid, params.Total, params.UnitName) + out = append(out, fmt.Sprintf("%d (%d %s)", curid, params.Total, params.UnitName)) } - fmt.Printf("]") + fmt.Printf("%s]", strings.Join(out, ", ")) } if len(acctInfo.AppParams) > 0 { - fmt.Printf("\t[created app IDs:") + fmt.Printf("\t[created app IDs: ") + var out []string for aid := range acctInfo.AppParams { - fmt.Printf(" %d", aid) + out = append(out, fmt.Sprintf("%d", aid)) } - fmt.Printf("]") + fmt.Printf("%s]", strings.Join(out, ", ")) } if len(acctInfo.AppLocalStates) > 0 { - fmt.Printf("\t[opted in app IDs:") + fmt.Printf("\t[opted in app IDs: ") + var out []string for aid := range acctInfo.AppLocalStates { - fmt.Printf(" %d", aid) + out = append(out, fmt.Sprintf("%d", aid)) } - fmt.Printf("]") + fmt.Printf("%s]", strings.Join(out, ", ")) } if accountList.isDefault(addr) { From 664885c98e7e3e055d064c15e154f6cd2b08d3e1 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 12:09:09 -0400 Subject: [PATCH 253/267] fix TestApplicationsUpgradeOverREST --- test/e2e-go/upgrades/application_support_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e-go/upgrades/application_support_test.go b/test/e2e-go/upgrades/application_support_test.go index 79643cfc8d..f8a1fc2917 100644 --- a/test/e2e-go/upgrades/application_support_test.go +++ b/test/e2e-go/upgrades/application_support_test.go @@ -142,7 +142,7 @@ int 1 ` approval, err := logic.AssembleString(counter) require.NoError(t, err) - clearstate, err := logic.AssembleString("int 1") + clearstate, err := logic.AssembleString("#pragma version 2\nint 1") require.NoError(t, err) schema := basics.StateSchema{ NumUint: 1, @@ -382,7 +382,7 @@ int 1 ` approval, err := logic.AssembleString(counter) require.NoError(t, err) - clearstate, err := logic.AssembleString("int 1") + clearstate, err := logic.AssembleString("#pragma version 2\nint 1") require.NoError(t, err) schema := basics.StateSchema{ NumUint: 1, From 63de13ced5f18648fc8560057f8bab1e6d4d9210 Mon Sep 17 00:00:00 2001 From: egieseke Date: Mon, 3 Aug 2020 12:10:10 -0400 Subject: [PATCH 254/267] Extended timeout for stateful teal expect tests. --- test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp | 1 + test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp | 1 + test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp b/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp index ff44fad3cb..d5fd16955c 100644 --- a/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp +++ b/test/e2e-go/cli/goal/expect/statefulTealAppInfoTest.exp @@ -10,6 +10,7 @@ set TEST_DATA_DIR [lindex $argv 1] proc statefulTealAppInfoTest { TEST_ALGO_DIR TEST_DATA_DIR} { + set timeout 60 set TIME_STAMP [clock seconds] set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP diff --git a/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp b/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp index 12f3f9a688..04a7d36dd9 100644 --- a/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp +++ b/test/e2e-go/cli/goal/expect/statefulTealAppReadTest.exp @@ -10,6 +10,7 @@ set TEST_DATA_DIR [lindex $argv 1] proc statefulTealAppReadTest { TEST_ALGO_DIR TEST_DATA_DIR} { + set timeout 60 set TIME_STAMP [clock seconds] set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP diff --git a/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp b/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp index 3a40f0241b..d2bd2c683f 100644 --- a/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp +++ b/test/e2e-go/cli/goal/expect/statefulTealCreateAppTest.exp @@ -11,7 +11,7 @@ set TEST_DATA_DIR [lindex $argv 1] proc statefulTealTest { TEST_ALGO_DIR TEST_DATA_DIR TEAL_PROGRAM} { - + set timeout 60 set TIME_STAMP [clock seconds] set TEST_ROOT_DIR $TEST_ALGO_DIR/root_$TIME_STAMP From 9eaa75e3caaef7f636c58bd06e8d971215e912b7 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 3 Aug 2020 12:38:39 -0400 Subject: [PATCH 255/267] Update test names per reviewer request. --- test/e2e-go/cli/algod/expect/algod_expect_test.go | 4 ++-- test/e2e-go/cli/algoh/expect/algoh_expect_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e-go/cli/algod/expect/algod_expect_test.go b/test/e2e-go/cli/algod/expect/algod_expect_test.go index 76e64996eb..bc269e892f 100644 --- a/test/e2e-go/cli/algod/expect/algod_expect_test.go +++ b/test/e2e-go/cli/algod/expect/algod_expect_test.go @@ -21,8 +21,8 @@ import ( "github.com/algorand/go-algorand/test/framework/fixtures" ) -// TesthWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algod/expect directory -func TestGoalWithExpect(t *testing.T) { +// TestAlgodWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algod/expect directory +func TestAlgodWithExpect(t *testing.T) { et := fixtures.MakeExpectTest(t) et.Run() } diff --git a/test/e2e-go/cli/algoh/expect/algoh_expect_test.go b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go index eb3fd9c5e8..52043da748 100644 --- a/test/e2e-go/cli/algoh/expect/algoh_expect_test.go +++ b/test/e2e-go/cli/algoh/expect/algoh_expect_test.go @@ -21,8 +21,8 @@ import ( "github.com/algorand/go-algorand/test/framework/fixtures" ) -// TesthWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algoh/expect directory -func TestGoalWithExpect(t *testing.T) { +// TestAlgohWithExpect Process all expect script files with suffix Test.exp within the test/e2e-go/cli/algoh/expect directory +func TestAlgohWithExpect(t *testing.T) { et := fixtures.MakeExpectTest(t) et.Run() } From cf7545e3de4912a93462ed89d02de47cf836f625 Mon Sep 17 00:00:00 2001 From: Max Justicz Date: Mon, 3 Aug 2020 12:59:33 -0400 Subject: [PATCH 256/267] unify print statements --- cmd/goal/accountsList.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/cmd/goal/accountsList.go b/cmd/goal/accountsList.go index e9d4630c68..20485c2762 100644 --- a/cmd/goal/accountsList.go +++ b/cmd/goal/accountsList.go @@ -226,28 +226,25 @@ func (accountList *AccountsList) outputAccount(addr string, acctInfo v1.Account, fmt.Printf("\t[%d/%d multisig]", multisigInfo.Threshold, len(multisigInfo.PKs)) } if len(acctInfo.AssetParams) > 0 { - fmt.Printf("\t[created asset IDs: ") var out []string for curid, params := range acctInfo.AssetParams { out = append(out, fmt.Sprintf("%d (%d %s)", curid, params.Total, params.UnitName)) } - fmt.Printf("%s]", strings.Join(out, ", ")) + fmt.Printf("\t[created asset IDs: %s]", strings.Join(out, ", ")) } if len(acctInfo.AppParams) > 0 { - fmt.Printf("\t[created app IDs: ") var out []string for aid := range acctInfo.AppParams { out = append(out, fmt.Sprintf("%d", aid)) } - fmt.Printf("%s]", strings.Join(out, ", ")) + fmt.Printf("\t[created app IDs: %s]", strings.Join(out, ", ")) } if len(acctInfo.AppLocalStates) > 0 { - fmt.Printf("\t[opted in app IDs: ") var out []string for aid := range acctInfo.AppLocalStates { out = append(out, fmt.Sprintf("%d", aid)) } - fmt.Printf("%s]", strings.Join(out, ", ")) + fmt.Printf("\t[opted in app IDs: %s]", strings.Join(out, ", ")) } if accountList.isDefault(addr) { From 94fe193cf7f38c4125427b8062399d4050c57789 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Mon, 3 Aug 2020 13:33:57 -0400 Subject: [PATCH 257/267] Testing script for Gossip and TxSync for Algorand Nov-2019 release --- .../gossip-txsync-upgrade/assert.sh | 312 +++ .../gossip-txsync-upgrade/functional-test.sh | 1874 +++++++++++++++++ .../network-config/three-nodes.json | 54 + .../network-config/two-nodes.json | 39 + .../consensus-version-next-proto.diff | 13 + .../nightly-1.1.304/enable-asset-create.diff | 59 + .../fast-upgrade-to-next-proto.diff | 26 + .../nightly-1.1.304/node-no-tx-broadcast.diff | 37 + .../rel/nightly-1.1.304/txpool-remember.diff | 14 + .../upgrade-path-to-next-proto.diff | 26 + .../consensus-version-next-proto.diff | 13 + .../fast-upgrade-to-next-proto | 16 + .../nightly-2.0.323/node-no-tx-broadcast.diff | 37 + .../rel/nightly-2.0.323/txpool-remember.diff | 14 + .../upgrade-path-to-next-proto.diff | 16 + .../stable-1.0.29/node-no-tx-broadcast.diff | 29 + .../rel/stable-1.0.29/txpool-remember.diff | 13 + 17 files changed, 2592 insertions(+) create mode 100644 test/release-testing/gossip-txsync-upgrade/assert.sh create mode 100755 test/release-testing/gossip-txsync-upgrade/functional-test.sh create mode 100644 test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json create mode 100644 test/release-testing/gossip-txsync-upgrade/network-config/two-nodes.json create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/consensus-version-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/enable-asset-create.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/fast-upgrade-to-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/node-no-tx-broadcast.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/txpool-remember.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/upgrade-path-to-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/consensus-version-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/fast-upgrade-to-next-proto create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/node-no-tx-broadcast.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/txpool-remember.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/upgrade-path-to-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/node-no-tx-broadcast.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/txpool-remember.diff diff --git a/test/release-testing/gossip-txsync-upgrade/assert.sh b/test/release-testing/gossip-txsync-upgrade/assert.sh new file mode 100644 index 0000000000..79bc4dcaab --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/assert.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash + +##################################################################### +## +## title: Assert Extension +## +## description: +## Assert extension of shell (bash, ...) +## with the common assert functions +## Function list based on: +## http://junit.sourceforge.net/javadoc/org/junit/Assert.html +## Log methods : inspired by +## - https://natelandau.com/bash-scripting-utilities/ +## author: Mark Torok +## +## date: 07. Dec. 2016 +## +## license: MIT +## +##################################################################### + +if command -v tput &>/dev/null; then + RED=$(tput setaf 1) + GREEN=$(tput setaf 2) + MAGENTA=$(tput setaf 5) + NORMAL=$(tput sgr0) + BOLD=$(tput bold) +else + RED=$(echo -en "\e[31m") + GREEN=$(echo -en "\e[32m") + MAGENTA=$(echo -en "\e[35m") + NORMAL=$(echo -en "\e[00m") + BOLD=$(echo -en "\e[01m") +fi + +log_header() { + printf "\n${BOLD}${MAGENTA}========== %s ==========${NORMAL}\n" "$@" >&2 +} + +log_success() { + printf "${GREEN}✔ %s${NORMAL}\n" "$@" >&2 +} + +log_failure() { + printf "${RED}✖ %s${NORMAL}\n" "$@" >&2 +} + + +assert_eq() { + local expected="$1" + local actual="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [ "$expected" == "$actual" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$expected == $actual :: $msg" || true + return 1 + fi +} + +assert_not_eq() { + local expected="$1" + local actual="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [ ! "$expected" == "$actual" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$expected != $actual :: $msg" || true + return 1 + fi +} + +assert_true() { + local actual + local msg + + actual="$1" + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + assert_eq true "$actual" "$msg" + return "$?" +} + +assert_false() { + local actual + local msg + + actual="$1" + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + assert_eq false "$actual" "$msg" + return "$?" +} + +assert_array_eq() { + + declare -a expected=("${!1}") + # echo "AAE ${expected[@]}" + + declare -a actual=("${!2}") + # echo "AAE ${actual[@]}" + + local msg + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + local return_code + return_code=0 + if [ ! "${#expected[@]}" == "${#actual[@]}" ]; then + return_code=1 + fi + + local i + for (( i=1; i < ${#expected[@]} + 1; i+=1 )); do + if [ ! "${expected[$i-1]}" == "${actual[$i-1]}" ]; then + return_code=1 + break + fi + done + + if [ "$return_code" == 1 ]; then + [ "${#msg}" -gt 0 ] && log_failure "(${expected[*]}) != (${actual[*]}) :: $msg" || true + fi + + return "$return_code" +} + +assert_array_not_eq() { + + declare -a expected=("${!1}") + declare -a actual=("${!2}") + + local msg + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + local return_code + return_code=1 + if [ ! "${#expected[@]}" == "${#actual[@]}" ]; then + return_code=0 + fi + + local i + for (( i=1; i < ${#expected[@]} + 1; i+=1 )); do + if [ ! "${expected[$i-1]}" == "${actual[$i-1]}" ]; then + return_code=0 + break + fi + done + + if [ "$return_code" == 1 ]; then + [ "${#msg}" -gt 0 ] && log_failure "(${expected[*]}) == (${actual[*]}) :: $msg" || true + fi + + return "$return_code" +} + +assert_empty() { + local actual + local msg + + actual="$1" + + if [ "$#" -ge 2 ]; then + msg="$2" + fi + + assert_eq "" "$actual" "$msg" + return "$?" +} + +assert_not_empty() { + local actual + local msg + + actual="$1" + + if [ "$#" -ge 2 ]; then + msg="$2" + fi + + assert_not_eq "" "$actual" "$msg" + return "$?" +} + +assert_contain() { + local haystack="$1" + local needle="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [ -z "${needle:+x}" ]; then + return 0; + fi + + if [ -z "${haystack##*$needle*}" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$haystack doesn't contain $needle :: $msg" || true + return 1 + fi +} + +assert_not_contain() { + local haystack="$1" + local needle="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [ -z "${needle:+x}" ]; then + return 0; + fi + + if [ "${haystack##*$needle*}" ]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$haystack contains $needle :: $msg" || true + return 1 + fi +} + +assert_gt() { + local first="$1" + local second="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [[ "$first" -gt "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first > $second :: $msg" || true + return 1 + fi +} + +assert_ge() { + local first="$1" + local second="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [[ "$first" -ge "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first >= $second :: $msg" || true + return 1 + fi +} + +assert_lt() { + local first="$1" + local second="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [[ "$first" -lt "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first < $second :: $msg" || true + return 1 + fi +} + +assert_le() { + local first="$1" + local second="$2" + local msg + + if [ "$#" -ge 3 ]; then + msg="$3" + fi + + if [[ "$first" -le "$second" ]]; then + return 0 + else + [ "${#msg}" -gt 0 ] && log_failure "$first <= $second :: $msg" || true + return 1 + fi +} diff --git a/test/release-testing/gossip-txsync-upgrade/functional-test.sh b/test/release-testing/gossip-txsync-upgrade/functional-test.sh new file mode 100755 index 0000000000..d8fc375933 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/functional-test.sh @@ -0,0 +1,1874 @@ +#!/bin/bash +set -e +set -o pipefail + +SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )" + +if [[ "$#" -ne 1 ]]; then + echo "Syntax: function-test " + exit 1 +fi + +# list releases +BASE_VERSION="1.0.29" +RELEASE_V1="rel/stable-${BASE_VERSION}" + +CURRENT_VERSION="1.1.304" +CURRENT_VERSION="2.0.323" +CURRENT="rel/nightly-${CURRENT_VERSION}" + +# Parameters +# git revision of the previous release +STABLE=$RELEASE_V1 +# git revision of the current (new) release +TESTING=$CURRENT + +# Protocol versions +V17="https://github.com/algorandfoundation/specs/tree/5615adc36bad610c7f165fa2967f4ecfa75125f0" +V19="https://github.com/algorandfoundation/specs/tree/03ae4eac54f1325377d0a2df62b5ef7cc08c5e18" + +BASE_PROTO=$V17 +NEXT_PROTO=$V19 + +# if testing against stable-1.0.29 release, ensure go-algorand is in $GOPATH/src/github.com/algorand +if [ "${STABLE}" = "${RELEASE_V1}" ]; then + if [[ $1 != *src/github.com/algorand* ]]; then + echo "For ${RELEASE_V1} go-algorand must be under go-path" 1>&2 + exit 1 + fi +fi + +SRC_DIR=$1 + +GOPATH=$(go env GOPATH) +GO_BIN="${GOPATH}/bin" + +NETWORK_DIR="${SCRIPTPATH}/tests/net" +ASSET_NAME="my_long_asset_name" +ASSET_TOKEN="tatok" + + +function revision_to_name() { + local revision=$1 + echo "${revision##*/}" +} + +function version_gt() { + test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; +} + +function git_cleanup() { + local current_branch_name=$1 + local target_branch_name=$2 + + git reset --hard + git checkout "$current_branch_name" + git branch -D "$target_branch_name" +} + +# Source patching helpers +# Expects to be called in git repo dir. +# Expects patches at ${SCRIPTPATH}/patch. + +# Enable logging in TxPool +function patch_log_txpool_remember() { + trace_if_needed "patch_log_txpool_remember" + local revision=$1 + local diff_file="txpool-remember.diff" + cp -r "${SCRIPTPATH}/patch/${revision}/$diff_file" ./ + git apply "$diff_file" +} + +# Change consensus version to future +function patch_change_current_consensus_version() { + trace_if_needed "patch_change_current_consensus_version" + local revision=$1 + local diff_file="consensus-version-next-proto.diff" + cp -r "${SCRIPTPATH}/patch/${revision}/$diff_file" ./ + git apply "$diff_file" +} + +# Change consensus version to future +function patch_disable_tx_broadcast() { + local revision=$1 + trace_if_needed patch_disable_tx_broadcast + local diff_file="node-no-tx-broadcast.diff" + cp -r "${SCRIPTPATH}/patch/${revision}/$diff_file" ./ + git apply "$diff_file" +} + +function patch_upgrade_path_to_next_proto() { + local revision=$1 + trace_if_needed patch_upgrade_path_to_next_proto + local diff_file="upgrade-path-to-next-proto.diff" + cp -r "${SCRIPTPATH}/patch/${revision}/$diff_file" ./ + git apply "$diff_file" +} + +function patch_fast_upgrade_to_next_proto() { + local revision=$1 + trace_if_needed patch_fast_upgrade_to_next_proto + local diff_file="fast-upgrade-to-next-proto.diff" + cp -r "${SCRIPTPATH}/patch/${revision}/$diff_file" ./ + git apply "$diff_file" +} + +function patch_agg_remember_and_disable_gossip() { + local revision=$1 + patch_log_txpool_remember $revision + patch_disable_tx_broadcast $revision +} + +function patch_agg_remember_and_upgrade_path() { + local revision=$1 + patch_log_txpool_remember $revision + patch_upgrade_path_to_next_proto $revision +} + +function patch_agg_remember_and_disable_gossip_and_upgrade_path() { + local revision=$1 + patch_log_txpool_remember $revision + patch_disable_tx_broadcast $revision + patch_upgrade_path_to_next_proto $revision +} + +function patch_agg_remember_and_fast_upgrade_to_next_proto() { + local revision=$1 + patch_log_txpool_remember $revision + patch_fast_upgrade_to_next_proto $revision +} + +function patch_agg_remember_and_disable_gossip_and_patch_fast_upgrade_to_next_proto() { + local revision=$1 + patch_log_txpool_remember $revision + patch_disable_tx_broadcast $revision + patch_fast_upgrade_to_next_proto $revision +} + +function patch_noop() { + return 0 +} + +function build_algorand_by_rev() { + local src_dir=$1 + local revision=$2 + local target_dir=$3 + local patcher=$4 + + pushd "$src_dir" + + local target_branch_name=$(revision_to_name $revision) + local current_branch_name="$(git rev-parse --abbrev-ref HEAD)" + + trap "git_cleanup $current_branch_name $target_branch_name" ERR + + git reset --hard + git checkout "$revision" -b "$target_branch_name" + + # if testing against stable-1.0.29 release, patch homebrew in dependencies installation + if [ "$revision" = "${RELEASE_V1}" ]; then + if [ "$(uname)" = "Darwin" ]; then + sed -i '.bak' -e 's|caskroom/cask|homebrew/cask|g' ./scripts/configure_dev.sh + sh ./scripts/configure_dev.sh + mv ./scripts/configure_dev.sh.bak ./scripts/configure_dev.sh + fi + fi + + # Patch sources. The caller provides a correct patcher + rm -f *.diff + $patcher "$revision" + + mkdir -p "$target_dir" + + # build + make install > "$target_dir/build.log" 2>&1 + + trap - ERR + + # restore to branch + git_cleanup $current_branch_name $target_branch_name + + popd + + bin_files=("algod" "carpenter" "goal" "kmd" "msgpacktool") + for bin in "${bin_files[@]}"; do + cp "${GO_BIN}/${bin}" $target_dir + if [ $? -ne 0 ]; then exit 1; fi + done +} + +function build_binaries() { + local src_dir=$1 + local revision=$2 + local bin_dir=$3 + local patcher=$4 + + echo "Building $revision with patch(es) $patcher to $bin_dir" + + if [ -z "$patcher" ]; then + patcher=patch_noop + fi + + rm -rf $bin_dir && mkdir -p "${bin_dir}" + build_algorand_by_rev "$src_dir" "$revision" "$bin_dir" "$patcher" +} + +function update_node_config() { + update_json_value "$1" "$2" "$3" +} + +function update_json_value() { + local file=$1 + local key=$2 + local value=$3 + + jq --argjson value $value '. + {'$key': $value}' "$file" >"$file.tmp" && mv "$file.tmp" "$file" +} + +function delete_from_json() { + local file=$1 + local key=$2 + + jq 'del(.'$key')' "$file" >"$file.tmp" && mv "$file.tmp" "$file" +} + +function generate_network() { + local bin_dir=$1 + local proto=$2 + + rm -rf "$NETWORK_DIR" + + "$bin_dir/goal" network create -r "$NETWORK_DIR" -n funtestnet -t "${SCRIPTPATH}/network-config/three-nodes.json" + + update_json_value "$NETWORK_DIR/genesis.json" "proto" '"'$proto'"' + for node in "$NETWORK_DIR"/*/; do + update_json_value "$node/genesis.json" "proto" '"'$proto'"' + done +} + +<< DESCRIPTION +Update all nodes config (config.json) in the network dir +Parameters: + network_dir - path to network + key - config option name + value - config option value +DESCRIPTION +function update_network_node_config() { + local network_dir=$1 + local key=$2 + local value=$3 + + for node in "$network_dir"/*/; do + update_json_value "$node/config.json" "$key" "$value" + done +} + +<< DESCRIPTION +Update all nodes config (config.json) in the network dir for gossip tests +Parameters: + network_dir - path to network +DESCRIPTION +function update_network_node_config_for_gossip() { + update_network_node_config "$network_dir" TxSyncIntervalSeconds 3600 + update_network_node_config "$network_dir" BaseLoggerDebugLevel 5 + update_network_node_config "$network_dir" IncomingConnectionsLimit -1 + update_network_node_config "$network_dir" Version 4 +} + +<< DESCRIPTION +Update all nodes config (config.json) in the network dir for txsync tests +Parameters: + network_dir - path to network +DESCRIPTION +function update_network_node_config_for_txsync() { + update_network_node_config "$network_dir" TxSyncIntervalSeconds 1 + update_network_node_config "$network_dir" BaseLoggerDebugLevel 5 + update_network_node_config "$network_dir" IncomingConnectionsLimit -1 + update_network_node_config "$network_dir" Version 4 +} + +function fresh_temp_net() { + local target_dir=$1 + + rm -rf $target_dir + cp -r "$NETWORK_DIR" $target_dir +} + +function remove_temp_net() { + local target_dir=$1 + rm -rf "$target_dir" +} + +function network_cleanup() { + local bin_dir=$1 + local network_dir=$2 + + network_stop "$bin_dir" "$network_dir" + remove_temp_net "$network_dir" +} + +# Starts nodes specified by nodes map. +# Returns sender's bin dir (by echoing) +function network_start() { + local nodes=( $1 ) + local network_dir=$2 + local sender_name=$3 + + local bin_dir + for item in "${nodes[@]}"; do + local node_name="${item%%:*}" + local node_bin_dir="${item##*:}" + + # set bin dir to some value to have some default + if [ -z "$bin_dir" ]; then + bin_dir="$node_bin_dir" + fi + + # then set to sender's path + if [ "$node_name" == "$sender_name" ]; then + bin_dir="$node_bin_dir" + fi + + "$node_bin_dir/goal" network start -r "$network_dir" --node "$network_dir/$node_name" 2>&1 1>/dev/null + local retval="$?" + if [ "$retval" -ne "0" ]; then + "$node_bin_dir/goal" network stop -r "$network_dir" + return 1 + fi + done + + if [ -z "$bin_dir" ]; then + echo "bin dir not found for $sender_name" >&2 + return 1 + fi + + echo "$bin_dir" + return 0 +} + +function network_stop() { + local bin_dir=$1 + local network_dir=$2 + + "$bin_dir/goal" network stop -r "$network_dir" +} + +function all_nodes_alive() { + local nodes=( $1 ) + local network_dir=$2 + + local failed + for item in "${nodes[@]}"; do + local node_name="${item%%:*}" + local node_bin_dir="${item##*:}" + + local output=$("$node_bin_dir/goal" node status -d "$network_dir/$node_name" | grep 'Last committed block') + if [ -n "$output" ]; then + trace_if_needed "Node $node_name looks good" + else + trace_if_needed "Node $node_name looks dead" + failed="1" + tail -20 "$network_dir/$node_name/node.log" + break + fi + done + + local retval=0 + if [ -n "$failed" ]; then + retval=1 + fi + + return $retval +} + +function extract_from_emit_info() { + local emit_info=( $1 ) + local prop=$2 + local expected + + for item in "${emit_info[@]}"; do + local key="${item%%:*}" + local value="${item##*:}" + local node_name="${value%%@*}" + local count="${value##*@}" + if [ "$key" == "$prop" ]; then + expected="$node_name" + fi + done + + if [ -z "$expected" ]; then + echo "prop $prop not found in ${emit_info[@]}" >&2 + return 1 + fi + + echo "$expected" +} + +function sender_name_from_emit_info() { + local emit_info=( $1 ) + extract_from_emit_info "$(echo ${emit_info[@]})" "snd" + return $? +} + +function proposer_name_from_emit_info() { + local emit_info=( $1 ) + extract_from_emit_info "$(echo ${emit_info[@]})" "prp" + return $? +} + +# Execute a command and wait for specific value +# Need to provide regex to capture the value and max timeout +function wait_for_value() { + local cmd=$1 + local value=$2 + local regex=$3 + local timeout=$4 + + local sleep_duration=5 + local time_spent=0 + local actual_value="" + while [ "$time_spent" -le "$timeout" ] && [ "$actual_value" != "$value" ]; do + local output=$($cmd) + [[ $output =~ $regex ]] + actual_value=${BASH_REMATCH[1]} + sleep $sleep_duration + ((time_spent=time_spent+$sleep_duration)) + done + + echo $actual_value +} + +function trace_if_needed() { + local comment=$1 + local output=$2 + local ret_code=$3 + + if [ -z "$ret_code" ]; then + ret_code=0 + fi + + if [ -n "$ALGODEBUG" ] || [ "$ret_code" -ne "0" ]; then + echo "$comment" + if [ -n "$output" ]; then + echo "Result: $output" + fi + if [ "$ret_code" -ne "0" ]; then + echo "Status: $ret_code" + fi + fi +} + +function log_error() { + local message=$1 + echo "$message" 1>&2 +} + +<< DESCRIPTION +Transaction submission well before upgrade with Gossip disabled (TxSync is on). +See submit_and_check for details. +DESCRIPTION +function test_pre_upgrade_txsync() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + local proto=$4 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + + update_network_node_config_for_txsync "$network_dir" + + local sender_name=$(sender_name_from_emit_info "$(echo ${emit_info[@]})") + local bin_dir=$(network_start "$(echo ${nodes[@]})" "$network_dir" "$sender_name") + trace_if_needed "Network started $network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + submit_and_check "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$proto" + local retval="$?" + local tx_sync_count="${__submit_and_check}" + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -eq "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected non-zero" + retval=1 + fi + + return $retval +} + +<< DESCRIPTION +Transaction submission well before upgrade with TxSync disabled (Gossip is on). +See submit_and_check for details. +DESCRIPTION +function test_pre_upgrade_gossip() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + local proto=$4 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + + update_network_node_config_for_gossip "$network_dir" + + local sender_name=$(sender_name_from_emit_info "$(echo ${emit_info[@]})") + local bin_dir=$(network_start "$(echo ${nodes[@]})" "$network_dir" "$sender_name") + trace_if_needed "Network started $network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + submit_and_check "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$proto" + local retval="$?" + local tx_sync_count="${__submit_and_check}" + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -ne "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected zero" + retval=1 + fi + + return $retval +} + +<< DESCRIPTION +Transaction submission after upgrade proposal with TxSync disabled (Gossip is on). +See txn_submit_after_proposal for details. +DESCRIPTION +function test_after_proposal_gossip() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + update_network_node_config_for_gossip "$network_dir" + + txn_submit_after_proposal "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" + local retval="$?" + local tx_sync_count="${__txn_submit_after_proposal}" + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -ne "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected zero" + retval=1 + fi + + return $retval +} + +<< DESCRIPTION +Transaction submission after upgrade proposal with Gossip disabled (TxSync is on). +See txn_submit_after_proposal for details. +DESCRIPTION +function test_after_proposal_txsync() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + update_network_node_config_for_txsync "$network_dir" + + txn_submit_after_proposal "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" + local retval="$?" + local tx_sync_count="${__txn_submit_after_proposal}" + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -eq "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected non-zero" + retval=1 + fi + + return $retval +} + +__txn_submit_after_proposal="0" + +<< DESCRIPTION +Transaction submission after upgrade proposal +Preconditions: +1. Network provisioned, config updated, but not started +Parameters: + nodes - an array (encoded map) of node names and binary paths + emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations + network_dir - a path to the directory with running network + tx_dir - a path to the directory with pre-generated transactions to submit +Return: + 0 on success + __txn_submit_after_proposal is set to amount of txsync requests found in the log + +Idea: +1. Wait for the upgrade proposal is assigned to a block +2. Submit transactions as usual +DESCRIPTION +function txn_submit_after_proposal() { + local nodes=( $1 ) + local emit_info=( $2 ) + local network_dir=$3 + local tx_dir=$4 + + local sender_name=$(sender_name_from_emit_info "$(echo ${emit_info[@]})") + local proposer_name=$(proposer_name_from_emit_info "$(echo ${emit_info[@]})") + local bin_dir=$(network_start "$(echo ${nodes[@]})" "$network_dir" "$sender_name") + trace_if_needed "Network started $network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + local cmd="$bin_dir/goal node status -d $network_dir/$proposer_name" + local regex='Next consensus protocol: ([a-zA-Z0-9:/.]+)' + local expected="$NEXT_PROTO" + local timeout="30" + local actual=$(wait_for_value "$cmd" "$expected" "$regex" "$timeout") + if [ -z "$actual" ]; then + log_error "Node $proposer_name has not accepted the upgrade in $timeout seconds" + false # abort and force trap + fi + + tx_dir="$tx_dir/$sender_name" + submit_and_check "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" + local retval="$?" + __txn_submit_after_proposal="${__submit_and_check}" + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" + + return $retval +} + +<< DESCRIPTION +Transaction submission after upgrade with TxSync disabled (Gossip is on). +See txn_submit_after_upgrade for details. +DESCRIPTION +function test_upgrade_applied_gossip() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + + update_network_node_config_for_gossip "$network_dir" + txn_submit_after_upgrade "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" + local retval="$?" + local tx_sync_count=${__txn_submit_after_upgrade} + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -ne "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected zero" + retval=1 + fi + + return $retval +} + +<< DESCRIPTION +Transaction submission after upgrade with Gossip disabled (TxSync is on). +See txn_submit_after_upgrade for details. +DESCRIPTION +function test_upgrade_applied_txsync() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + + update_network_node_config_for_txsync "$network_dir" + txn_submit_after_upgrade "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" + local retval="$?" + local tx_sync_count=${__txn_submit_after_upgrade} + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -eq "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected non-zero" + retval=1 + fi + + return $retval +} + +__txn_submit_after_upgrade="0" + +<< DESCRIPTION +Transaction submission after upgrade +Preconditions: +1. Network provisioned, config updated, but not started +Parameters: + nodes - an array (encoded map) of node names and binary paths + emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations + network_dir - a path to the directory with running network + tx_dir - a path to the directory with pre-generated transactions to submit +Return: + 0 on success + __txn_submit_after_upgrade is set to amount of txsync requests found in the log + +Idea: +1. Wait for the upgrade +2. Submit transactions as usual +DESCRIPTION +function txn_submit_after_upgrade() { + local nodes=( $1 ) + local emit_info=( $2 ) + local network_dir=$3 + local tx_dir=$4 + + local sender_name=$(sender_name_from_emit_info "$(echo ${emit_info[@]})") + local bin_dir=$(network_start "$(echo ${nodes[@]})" "$network_dir" "$sender_name") + trace_if_needed "Network started $network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + local cmd="$bin_dir/goal node status -d $network_dir/$sender_name" + local regex='Last consensus protocol: ([a-zA-Z0-9:/.]+)' + local expected="$NEXT_PROTO" + local timeout="60" + local actual=$(wait_for_value "$cmd" "$expected" "$regex" "$timeout") + if [ -z "$actual" ]; then + log_error "Node $sender_name has not upgraded in $timeout seconds" + false # abort and force trap + fi + + local proto="$actual" + tx_dir="$tx_dir/$sender_name" + submit_and_check "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$proto" + local retval="$?" + __txn_submit_after_upgrade="${__submit_and_check}" + + # ensure all nodes alive (no crashes) + all_nodes_alive "$(echo ${nodes[@]})" "$network_dir" + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" + + return $retval +} + +<< DESCRIPTION +Transaction submission around the upgrade round with TxSync disabled (Gossip is on). +See txn_submit_at_round for details. +DESCRIPTION +function test_at_upgrade_gossip() { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + local upgrade_round=$4 + local submit_round=$5 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + update_network_node_config_for_gossip "$network_dir" + + txn_submit_at_round "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$upgrade_round" "$submit_round" + local retval="$?" + local tx_sync_count=${__txn_submit_at_round} + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -ne "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected zero" + retval=1 + fi + + return $retval +} + +<< DESCRIPTION +Transaction submission around the upgrade round with Gossip disabled (TxSync is on). +See txn_submit_at_round for details. +DESCRIPTION +function test_at_upgrade_txsync { + local nodes=( $1 ) + local emit_info=( $2 ) + local tx_dir=$3 + local upgrade_round=$4 + local submit_round=$5 + + local test_name="${FUNCNAME[0]}" + local base_dir="${SCRIPTPATH}/tests/${test_name}" + local network_dir="${base_dir}/net" + mkdir -p $base_dir + + fresh_temp_net "$network_dir" + update_network_node_config_for_txsync "$network_dir" + + txn_submit_at_round "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$upgrade_round" "$submit_round" + local retval="$?" + local tx_sync_count=${__txn_submit_at_round} + + if [ "$retval" -ne "0" ] || [ "$tx_sync_count" -eq "0" ]; then + log_error "$test_name failed: txsync count is $tx_sync_count but expected non-zero" + retval=1 + fi + + return $retval +} + +__txn_submit_at_round="0" + +<< DESCRIPTION +Transaction submission around the upgrade round with TxSync disabled (Gossip is on). +Preconditions: +1. Network provisioned, config updated, but not started +Parameters: + nodes - an array (encoded map) of node names and binary paths + emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations + network_dir - a path to the directory with running network + tx_dir - a path to the directory with pre-generated transactions to submit + upgrade_round - a round number when upgrade is expected to happen + submit_round - a round number for transaction submission +Return: + 0 on success + __txn_submit_at_round is set to amount of txsync requests found in the log + +Idea: +1. Wait for the upgrade approval +2. Ensure that last round is less then upgrade_round and submit_round so there is a time window to submit transactions +3. Wait for end of submit_round +4. Submit transactions as usual + +The function is designed to validate two scenarios: + - submitting at upgrade round - set upgrade_round=5 and submit_round=5 + - submitting at upgrade+1 round - set upgrade_round=5 and submit_round=6s +DESCRIPTION +function txn_submit_at_round() { + local nodes=( $1 ) + local emit_info=( $2 ) + local network_dir=$3 + local tx_dir=$4 + local upgrade_round=$5 + local submit_round=$6 + + local sender_name=$(sender_name_from_emit_info "$(echo ${emit_info[@]})") + local bin_dir=$(network_start "$(echo ${nodes[@]})" "$network_dir" "$sender_name") + trace_if_needed "Network started $network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + local token=$(cat "$network_dir/$sender_name/algod.token") + local node_address=$(cat "$network_dir/$sender_name/algod.net") + local info_url="http://$node_address/v1/status" + local next_proto="some_random_value_123" + local next_proto_round="0" + local last_round="0" + local timeout="60" + local sleep_duration="0.01" + local time_spent="0" + local time_spent_act="0" + + trace_if_needed "Waiting for upgrade... expected at $upgrade_round, will be submitting at the beginning of $submit_round" + while [ "$time_spent" -le "$timeout" ] && [ "$next_proto" != "$NEXT_PROTO" ]; do + local output=$(curl -s -H "X-ALGO-API-Token: $token" $info_url) + next_proto=$(echo $output | jq -r '.nextConsensusVersion') + next_proto_round=$(echo $output | jq -r '.nextConsensusVersionRound') + last_round=$(echo $output | jq -r '.lastRound') + + sleep $sleep_duration + time_spent_act=$(echo $time_spent_act + $sleep_duration | bc | awk '{printf "%.1f\n", $0}') + time_spent=${time_spent_act%.*} + done + + if [ "$next_proto_round" -ne "$upgrade_round" ]; then + log_error "Upgrade round is expected to be $upgrade_round but actual is $next_proto_round" + return 1 + fi + + local current_round="0" + ((current_round=last_round+1)) + if [ "$current_round" -gt "$submit_round" ]; then + log_error "Last round $last_round (current $current_round) is too high, needs to be below submit round $submit_round" + return 1 + fi + + local pre_submit_round="0" + ((pre_submit_round=submit_round-1)) + local time_since_last_round="0" + local round_duration="4000000000" # nanoseconds + trace_if_needed "Catching current round == $pre_submit_round and time since < $round_duration ns" + while [ "$time_spent" -le "$timeout" ] && \ + ( \ + [ "$current_round" -lt "$pre_submit_round" ] || \ + [ "$current_round" -eq "$pre_submit_round" ] && [ "$time_since_last_round" -le "$round_duration" ] \ + ) + do + local output=$(curl -s -H "X-ALGO-API-Token: $token" $info_url) + time_since_last_round=$(echo $output | jq -r '.timeSinceLastRound') + last_round=$(echo $output | jq -r '.lastRound') + ((current_round=last_round+1)) + + sleep $sleep_duration + time_spent_act=$(echo $time_spent_act + $sleep_duration | bc | awk '{printf "%.1f\n", $0}') + time_spent=${time_spent_act%.*} + done + + if [ "$time_spent" -gt "$timeout" ]; then + log_error "Failed to current ($current_round) <= pre ($pre_submit_round) and $time_since_last_round < $round_duration" + return 1 + fi + + local output=$(curl -s -H "X-ALGO-API-Token: $token" $info_url) + last_round=$(echo $output | jq -r '.lastRound') + time_since_last_round=$(echo $output | jq -r '.timeSinceLastRound') + + trace_if_needed "Sending at last round $last_round and time since $time_since_last_round" + + submit_and_check "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$tx_dir" "$NEXT_PROTO" + local retval="$?" + __txn_submit_at_round=${__submit_and_check} + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" + + return $retval +} + + +__submit_and_check=0 + +<< DESCRIPTION +Submit all the transactions from the directory. +It checks sender and receiver node logs (see emit_info) and count transactions +Preconditions: + 1. Binaries are instrumented + 2. Network is running +Parameters: + nodes - an array (encoded map) of node names and binary paths + emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations + network_dir - a path to the directory with running network + tx_dir - a path to the directory with pre-generated transactions to submit + proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit asset transaction +Returns: + 0 on success + __submit_and_check is set to amount of txsync requests found in the log + +The function is designed to count TX remember calls (instrumented logging) and TxSync calls. +DESCRIPTION +function submit_and_check() { + + local nodes=( $1 ) + local emit_info=( $2 ) + local network_dir=$3 + local tx_dir=$4 + local proto=$5 + + local sender_name + local receiver_name + local sender_count + local receiver_count + + for item in "${emit_info[@]}"; do + local key="${item%%:*}" + local value="${item##*:}" + local node_name="${value%%@*}" + local count="${value##*@}" + + if [ "$key" == "snd" ]; then + sender_name="$node_name" + sender_count="$count" + + fi + if [ "$key" == "rcv" ]; then + receiver_name="$node_name" + receiver_count="$count" + fi + done + + if [ -z "$sender_name" ] || [ -z "$receiver_name" ]; then + echo "Error: snd or rcv not set in ${emit_info[@]}" + return 1 + fi + + if [ -z "$sender_count" ] || [ -z "$receiver_count" ]; then + echo "Error: expected count not set in ${emit_info[@]}" + return 1 + fi + + "$bin_dir/goal" node wait -w 30 -d "$network_dir/$sender_name" + + local txids=() + for f in "$tx_dir"/*.stx; do + # Send and parse tx ID from 'Raw transaction ID 5ORN5BDZX2WPT6VGISKXZC4UMW6WWS7ZTCPDAW33VTKDAPOHZEVQ issued' + local output=$("$bin_dir/goal" clerk rawsend -f $f -N -d "$network_dir/$sender_name") + local regex='Raw transaction ID ([A-Z0-9=]{52,52})' + [[ $output =~ $regex ]] + local txid=${BASH_REMATCH[1]} + # local txid=$(echo $output | head -1 | awk '{ print $4 }') + if [ -z "$txid" ]; then + log_error "$output" + else + txids+=($txid) + fi + done + + check_asset_transactions "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$proto" + local asset_txids=( ${__check_asset_transactions[@]} ) + local txids=( "${txids[@]}" "${asset_txids[@]}" ) + + # wait to let all tx get propagated + "$bin_dir/goal" node wait -w 30 -d "$network_dir/$sender_name" + + local node1=0 + local node2=0 + local txsync_request=$(grep "http sync got" "$network_dir/$receiver_name/node.log" | grep -c '^') + for txid in "${txids[@]}"; do + trace_if_needed "Looking for tx ID $txid" + + local node1_lines=$(grep "Transaction remembered $txid" "$network_dir/$sender_name/node.log" | grep -c '^') + local node2_lines=$(grep "Transaction remembered $txid" "$network_dir/$receiver_name/node.log" | grep -c '^') + + ((node1=node1+node1_lines)) + ((node2=node2+node2_lines)) + done + + trace_if_needed "Sender ($sender_name): found $node1, expected $sender_count" + trace_if_needed "Receiver ($receiver_name): found $node2, expected $receiver_count" + + local retval=0 + if [ "$sender_count" -ne "$node1" ] || [ "$receiver_count" -ne "$node2" ]; then + echo "${FUNCNAME[0]} error: $sender_count != $node1 OR $receiver_count != $node2" + retval=1 + fi + + # return two values - one in the global var and the second as a regular ret code + __submit_and_check=$txsync_request + return $retval +} + +# return random value from [1000, 1500) +function get_random_fee() { + echo "$((RANDOM % 500 + 1000))" +} + +<< DESCRIPTION +Pre-generate transactions using specified binaries into a dir provided. +Parameters: + bin_dir - a path to algod/goal binaries + sender_name - sender's node name + receiver_name - receiver's node name + tx_dir - a path to the directory where store transactions to + proto - a protocol version is being used. "$NEXT_PROTO" triggers waiting for an upgrade + valid_round - firstvalid and lastvalid set to this value +DESCRIPTION +function generate_transactions() { + local bin_dir=$1 + local sender_name=$2 + local receiver_name=$3 + local tx_dir=$4 + local proto=$5 + local valid_round=$6 + + local network_dir="${tx_dir}/net" + mkdir -p "$tx_dir" + + local alogd_version="$("$bin_dir/algod" -v | sed -n 2p | cut -f 1 -d ' ' | cut -f 1-3 -d .)" + + fresh_temp_net "$network_dir" + + trap "network_cleanup $bin_dir $network_dir" ERR + + "$bin_dir/goal" network start -r "$network_dir" + "$bin_dir/goal" node wait -w 30 -d "$network_dir/Primary" + + if [ "$proto" == "$NEXT_PROTO" ]; then + local cmd="$bin_dir/goal node status -d $network_dir/$sender_name" + local regex='Last consensus protocol: ([a-zA-Z0-9:/.]+)' + local expected="$NEXT_PROTO" + local timeout="60" + local actual=$(wait_for_value "$cmd" "$expected" "$regex" "$timeout") + if [ -z "$actual" ]; then + log_error "Node $sender_name has not upgraded in $timeout seconds" + false # abort and force trap + fi + fi + + local firstvalid="1" + local lastvalid="1000" + local validrounds="$lastvalid" + if [ -n "$valid_round" ]; then + firstvalid="$valid_round" + lastvalid="$valid_round" + validrounds="1" # well, it should be 0 but goal interprets 0 as max validity=1000 + fi + + local src_addr=$("$bin_dir/goal" account list -d "$network_dir/$sender_name" | head -1 | awk '{ print $2 }') + local dst_addr=$("$bin_dir/goal" account list -d "$network_dir/$receiver_name" | head -1 | awk '{ print $2 }') + + trace_if_needed "Creating payset tx" + local fee=$(get_random_fee) + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -i "$tx_dir/payset.tx" -o "$tx_dir/payset.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating keyreg tx" + fee=$(get_random_fee) + "$bin_dir/goal" account changeonlinestatus --fee $fee --address $src_addr -o -t "$tx_dir/keyreg.tx" --firstRound "$firstvalid" --validRounds "$validrounds" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -i "$tx_dir/keyreg.tx" -o "$tx_dir/keyreg.stx" -d "$network_dir/$sender_name" + + if version_gt "$alogd_version" "$BASE_VERSION" ; then + if [ "$proto" == "$NEXT_PROTO" ]; then + trace_if_needed "Creating logic sig payset tx" + fee=$(get_random_fee) + echo "int 1" > "$tx_dir/int1.teal" + # local escrow_addr="$("$bin_dir/goal" clerk compile "$tx_dir/int1.teal" | cut -f 2 -d ':' | sed -e 's/[[:space:]]//g')" + "$bin_dir/goal" clerk compile "$tx_dir/int1.teal" -s -a $src_addr -o "$tx_dir/int1.lsig" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-teal.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -L "$tx_dir/int1.lsig" -i "$tx_dir/payset-teal.tx" -o "$tx_dir/payset-teal.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating payset group tx" + fee=$(get_random_fee) + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-again.tx" -d "$network_dir/$sender_name" + cat "$tx_dir/payset.tx" "$tx_dir/payset-again.tx" > "$tx_dir/payset-concat.tx" + "$bin_dir/goal" clerk group -i "$tx_dir/payset-concat.tx" -o "$tx_dir/payset-group.tx" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/payset-group.tx" -o "$tx_dir/payset-group.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating asset create tx" + fee=$(get_random_fee) + "$bin_dir/goal" asset create --fee $fee --creator $src_addr --name $ASSET_NAME --total 100 --unitname $ASSET_TOKEN --firstvalid "$firstvalid" --validrounds "$validrounds" -o "$tx_dir/asset-create.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/asset-create.tx" -o "$tx_dir/asset-create.stx" -d "$network_dir/$sender_name" + fi + fi + + trap - ERR + network_cleanup "$bin_dir" "$network_dir" +} + +__check_asset_transactions=() + +<< DESCRIPTION +Submits asset transactions +Parameters: + nodes - an array (encoded map) of node names and binary paths + emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations + network_dir - a path to the directory with running network + proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit asset transaction +Returns: + 0 on success + __check_asset_transactions contains an array of submitted tx ids + +DESCRIPTION +function check_asset_transactions() { + local nodes=( $1 ) + local emit_info=( $2 ) + local network_dir=$3 + local proto=$4 + + if [ "$proto" != "$NEXT_PROTO" ]; then + return + fi + + local sender_name + local receiver_name + + for item in "${emit_info[@]}"; do + local key="${item%%:*}" + local value="${item##*:}" + local node_name="${value%%@*}" + local count="${value##*@}" + + if [ "$key" == "snd" ]; then + sender_name="$node_name" + + fi + if [ "$key" == "rcv" ]; then + receiver_name="$node_name" + fi + done + + local bin_dir + local primary_bin_dir + for item in "${nodes[@]}"; do + local node_name="${item%%:*}" + local node_bin_dir="${item##*:}" + if [ "$node_name" == "$sender_name" ]; then + bin_dir="$node_bin_dir" + fi + if [ "$node_name" == "Primary" ]; then + primary_bin_dir="$node_bin_dir" + fi + done + + local alogd_version="$("$bin_dir/algod" -v | sed -n 2p | cut -f 1 -d ' ' | cut -f 1-3 -d .)" + + local txids=() + if version_gt "$alogd_version" "$BASE_VERSION"; then + local src_addr=$("$bin_dir/goal" account list -d "$network_dir/$sender_name" | head -1 | awk '{ print $2 }') + local dst_addr=$("$bin_dir/goal" account list -d "$network_dir/$receiver_name" | head -1 | awk '{ print $2 }') + + "$bin_dir/goal" node wait -w 30 -d "$network_dir/$sender_name" + "$bin_dir/goal" node wait -w 30 -d "$network_dir/$receiver_name" + + local regex='txid ([A-Z0-9=]{52,52})' + # Issued transaction from account AKMIEYU64TDLTDER6LTGPRMAXUMDQQFKVMH5QTYECBWIXT7V4KZ6GFTI2I, txid ITJPKM7JW57HTJUWIZH3VDUYJ6VVAHPASMKGKIFXZT2P6M3IEKAA (fee 1000) + + trace_if_needed "Sending asset config tx" + local output=$("$bin_dir/goal" asset config --asset $ASSET_TOKEN --creator $src_addr --manager $src_addr --new-manager $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") + trace_if_needed "Sent asset config tx" "$output" $? + + [[ $output =~ $regex ]] + local txid=${BASH_REMATCH[1]} + trace_if_needed $txid + txids+=($txid) + + trace_if_needed "Sending asset send tx" + local output=$("$bin_dir/goal" asset send --asset $ASSET_TOKEN --creator $src_addr -a 1 -f $src_addr -t $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") + trace_if_needed "Sent asset send tx" "$output" $? + + [[ $output =~ $regex ]] + local txid=${BASH_REMATCH[1]} + trace_if_needed $txid + txids+=($txid) + + trace_if_needed "Sending asset freeze tx" + local output=$("$bin_dir/goal" asset freeze --asset $ASSET_TOKEN --creator $src_addr --freezer $src_addr --account $src_addr -N --freeze --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") + trace_if_needed "Sent asset freeze tx" "$output" $? + + [[ $output =~ $regex ]] + local txid=${BASH_REMATCH[1]} + trace_if_needed $txid + txids+=($txid) + + trace_if_needed "Sending asset destroy tx" + local output=$("$bin_dir/goal" asset destroy --asset $ASSET_TOKEN --creator $src_addr --manager $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") + trace_if_needed "Sent asset destroy tx" "$output" $? + + [[ $output =~ $regex ]] + local txid=${BASH_REMATCH[1]} + trace_if_needed $txid + txids+=($txid) + fi + + __check_asset_transactions=( ${txids[@]} ) +} + +TESTING_BIN_DIR="${SCRIPTPATH}/tests/$(revision_to_name $TESTING)/bin" +STABLE_BIN_DIR="${SCRIPTPATH}/tests/$(revision_to_name $STABLE)/bin" +VANILLA_TESTING_BIN_DIR="${SCRIPTPATH}/tests/$(revision_to_name $TESTING)-vanilla/bin" +VANILLA_STABLE_BIN_DIR="${SCRIPTPATH}/tests/$(revision_to_name $STABLE)-vanilla/bin" + +UPGRADE_ROUND="5" # matches to the code patch +((UPGRADE_ROUND_NEXT=UPGRADE_ROUND+1)) + +TX_BASE_DIR="${SCRIPTPATH}/tests/tx" +mkdir -p $TX_BASE_DIR + +if [ ! -d "$VANILLA_TESTING_BIN_DIR" ]; then + build_binaries "$SRC_DIR" "$TESTING" "$VANILLA_TESTING_BIN_DIR" +fi +if [ ! -d "$VANILLA_STABLE_BIN_DIR" ]; then + build_binaries "$SRC_DIR" "$STABLE" "$VANILLA_STABLE_BIN_DIR" +fi + +test_new_pre_upgrade="" +test_old_pre_upgrade="" +test_new_upgrade_proposed="" +test_new_upgrade_applied="1" +test_at_upgrade_round="1" +test_next_proto_standalone="1" +test_old_node_after_upgrade="" + +# generate sample network once to have the same genesis for all subsequent tests (except the separate future test) +generate_network "$VANILLA_TESTING_BIN_DIR" "$V17" + +# pre-generate old transactions +SENDER="Node1" +RECEIVER="Primary" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" + +# generate tx with valid round=UPGRADE_ROUND for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND" + +# generate tx with valid round=UPGRADE_ROUND+1 for at upgrade+1 test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND_NEXT" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE transaction with validity round=$UPGRADE_ROUND_NEXT" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND_NEXT" + +SENDER="Primary" +RECEIVER="Node2" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" + +# generate tx with valid round=UPGRADE_ROUND for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND" + +# generate tx with valid round=UPGRADE_ROUND+1 for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND_NEXT" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND_NEXT" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND_NEXT" + +SENDER="Node2" +RECEIVER="Primary" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" + +# upgrade network and pre-generate new transactions +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_fast_upgrade_to_next_proto + +SENDER="Node1" +RECEIVER="Primary" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING transaction" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" + +# generate tx with valid round=UPGRADE_ROUND for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$UPGRADE_ROUND" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING with validity round=$UPGRADE_ROUND" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" "$UPGRADE_ROUND" + +# generate tx with valid round=UPGRADE_ROUND+1 for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$UPGRADE_ROUND_NEXT" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING with validity round=$UPGRADE_ROUND_NEXT" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" "$UPGRADE_ROUND_NEXT" + +SENDER="Primary" +RECEIVER="Node2" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" + +# generate tx with valid round=5 for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$UPGRADE_ROUND" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING with validity round=$UPGRADE_ROUND" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" "$UPGRADE_ROUND" + +# generate tx with valid round=UPGRADE_ROUND+1 for at upgrade test +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$UPGRADE_ROUND_NEXT" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING with validity round=$UPGRADE_ROUND_NEXT" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" "$UPGRADE_ROUND_NEXT" + +SENDER="Node2" +RECEIVER="Primary" +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING transaction" +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" + +# Tests +# Note set -e handles exit after each failed tests +if [ -n "$test_new_pre_upgrade" ]; then +echo "=============================================================================================" +echo "1. Check v1.1.0 accepts old and new basic transactions (pre upgrade)" +echo + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$TESTING_BIN_DIR" +) +COUNT=2 + +echo "---------------------------------------------------------------------------------------------" +echo "1.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_log_txpool_remember + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +test_pre_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +test_pre_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +echo "---------------------------------------------------------------------------------------------" +echo "1.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER (because TxSync pulls only from Relays)" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" +fi + +if [ -n "$test_old_pre_upgrade" ]; then +echo "=============================================================================================" +echo "2. Check v1.0.29 accepts old and new basic transactions (pre upgrade)" +echo + +NODES=( + "Primary:$STABLE_BIN_DIR" + "Node1:$STABLE_BIN_DIR" + "Node2:$STABLE_BIN_DIR" +) +COUNT=2 + +echo "---------------------------------------------------------------------------------------------" +echo "2.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_log_txpool_remember + +# test old binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +test_pre_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test old binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +test_pre_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +echo "---------------------------------------------------------------------------------------------" +echo "2.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER (because TxSync pulls only from Relays)" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_agg_remember_and_disable_gossip + +# test old binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" +test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test old binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER" +test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" +fi + +if [ -n "$test_new_upgrade_proposed" ]; then +echo "=============================================================================================" +echo "3. Check v1.1.0 and v1.0.29 accept old and new basic transactions (upgrade proposed)" +echo + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$STABLE_BIN_DIR" +) +COUNT=2 +PROPOSER="Node1" + +echo "---------------------------------------------------------------------------------------------" +echo "3.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" + "prp:$PROPOSER@0" +) + +build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_log_txpool_remember +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_upgrade_path + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_after_proposal_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_after_proposal_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +SENDER="Node2" +RECEIVER="Primary" +echo "Now be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" + "prp:$PROPOSER@0" +) +# test old binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_after_proposal_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test old binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_after_proposal_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +echo "---------------------------------------------------------------------------------------------" +echo "3.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER (because TxSync pulls only from Relays)" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" + "prp:$PROPOSER@0" +) + +build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_agg_remember_and_disable_gossip +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip_and_upgrade_path + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_after_proposal_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_after_proposal_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# because TxSync pulls only from Relays, set Primary to be on the binaries +NODES=( + "Primary:$STABLE_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$STABLE_BIN_DIR" +) +echo "Changing network to ${NODES[@]}" + +# test old binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_after_proposal_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test old binaries with old transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_after_proposal_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" +fi + +if [ -n "$test_new_upgrade_applied" ]; then +echo "=============================================================================================" +echo "4. Check v1.1.0 accepts all old and new transactions (post upgrade)" +echo + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$TESTING_BIN_DIR" +) +COUNT=9 + +echo "---------------------------------------------------------------------------------------------" +echo "4.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_fast_upgrade_to_next_proto + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +EMIT_INFO=( + "snd:$SENDER@2" + "rcv:$RECEIVER@2" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +echo "---------------------------------------------------------------------------------------------" +echo "4.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER (because TxSync pulls only from Relays)" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip_and_patch_fast_upgrade_to_next_proto + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_upgrade_applied_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" + +# test new binaries with old transactions +EMIT_INFO=( + "snd:$SENDER@2" + "rcv:$RECEIVER@2" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" +test_upgrade_applied_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" +fi + +if [ -n "$test_at_upgrade_round" ]; then +echo "=============================================================================================" +echo "5. Check v1.1.0 accepts all old and new transactions (upgrade and upgrade+1)" +echo + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$TESTING_BIN_DIR" +) +COUNT=2 +((SUBMIT_ROUND=$UPGRADE_ROUND-1)) + +echo "---------------------------------------------------------------------------------------------" +echo "5.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_fast_upgrade_to_next_proto + +trace_if_needed "check upgrade round" +# submit a the beginning of UPGRADE_ROUND, so that transactions go to UPGRADE_ROUND+1 block +((SUBMIT_ROUND=$UPGRADE_ROUND-1)) +((TX_VALID_ROUND=$UPGRADE_ROUND)) + +trace_if_needed "test new binaries with old transactions" +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "test new binaries with old transactions" +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "check upgrade+1 round" +# submit a the beginning of UPGRADE_ROUND, so that transactions go to UPGRADE_ROUND+1 block +SUBMIT_ROUND="$UPGRADE_ROUND" +((TX_VALID_ROUND=$UPGRADE_ROUND+1)) + +trace_if_needed "test new binaries with old transactions" +COUNT=9 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "test new binaries with old transactions" +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +echo "---------------------------------------------------------------------------------------------" +echo "5.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER (because TxSync pulls only from Relays)" +echo + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip_and_patch_fast_upgrade_to_next_proto + +trace_if_needed "check upgrade round" +# submit a the beginning of UPGRADE_ROUND-1, so that transactions go to UPGRADE_ROUND block +((SUBMIT_ROUND=$UPGRADE_ROUND-1)) +((TX_VALID_ROUND=$UPGRADE_ROUND)) +# test new binaries with old transactions +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "test new binaries with old transactions" +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "check upgrade+1 round" +# submit a the beginning of UPGRADE_ROUND, so that transactions go to UPGRADE_ROUND+1 block +SUBMIT_ROUND="$UPGRADE_ROUND" +((TX_VALID_ROUND=$UPGRADE_ROUND+1)) + +trace_if_needed "test new binaries with old transactions" +COUNT=9 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" + +trace_if_needed "test new binaries with old transactions" +COUNT=2 +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) +TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$TX_VALID_ROUND" +test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" +fi + +if [ -n "$test_next_proto_standalone" ]; then +echo "=============================================================================================" +echo "6. Check v1.1.0 accepts all transactions (create a fresh v=NEXT_PROTO network)" +echo "This test re-generates the network so that no pre-generated transactions are valid" + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$TESTING_BIN_DIR" +) +COUNT=9 + +generate_network "$VANILLA_TESTING_BIN_DIR" "$NEXT_PROTO" + +echo "---------------------------------------------------------------------------------------------" +echo "6.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_change_current_consensus_version +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_log_txpool_remember +test_pre_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$NEXT_PROTO" + +echo "---------------------------------------------------------------------------------------------" +echo "6.2. Gossip tx broadcast disabled" + +SENDER="Primary" +RECEIVER="Node2" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_change_current_consensus_version +generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" + +build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip +test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$NEXT_PROTO" +fi + + +if [ -n "$test_old_node_after_upgrade" ]; then +echo "=============================================================================================" +echo "7. Check v1.0.29 does not crash after upgrade (post upgrade)" +echo + +NODES=( + "Primary:$TESTING_BIN_DIR" + "Node1:$TESTING_BIN_DIR" + "Node2:$STABLE_BIN_DIR" +) +COUNT=9 + +echo "---------------------------------------------------------------------------------------------" +echo "7.1. TxSync disabled" + +SENDER="Node1" +RECEIVER="Primary" +echo "Will be sending from $SENDER to $RECEIVER" +echo + +EMIT_INFO=( + "snd:$SENDER@$COUNT" + "rcv:$RECEIVER@$COUNT" +) + +# build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_log_txpool_remember +# build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_fast_upgrade_to_next_proto + +# test new binaries with new transactions +TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" +test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" +fi diff --git a/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json b/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json new file mode 100644 index 0000000000..dad8b0021a --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json @@ -0,0 +1,54 @@ +{ + "Genesis": { + "NetworkName": "funtestnetls", + "Wallets": [ + { + "Name": "WalletP", + "Stake": 1, + "Online": true + }, + { + "Name": "Wallet1", + "Stake": 98, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 1, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [ + { + "Name": "WalletP", + "ParticipationOnly": false + } + ] + }, + { + "Name": "Node1", + "IsRelay": false, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + } + ] + }, + { + "Name": "Node2", + "IsRelay": false, + "Wallets": [ + { + "Name": "Wallet2", + "ParticipationOnly": false + } + ] + } + ] +} diff --git a/test/release-testing/gossip-txsync-upgrade/network-config/two-nodes.json b/test/release-testing/gossip-txsync-upgrade/network-config/two-nodes.json new file mode 100644 index 0000000000..e8d7ebd9bb --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/network-config/two-nodes.json @@ -0,0 +1,39 @@ +{ + "Genesis": { + "NetworkName": "funtestnetls", + "Wallets": [ + { + "Name": "Wallet1", + "Stake": 99, + "Online": true + }, + { + "Name": "Wallet2", + "Stake": 1, + "Online": true + } + ] + }, + "Nodes": [ + { + "Name": "Primary", + "IsRelay": true, + "Wallets": [ + { + "Name": "Wallet1", + "ParticipationOnly": false + } + ] + }, + { + "Name": "Node", + "IsRelay": false, + "Wallets": [ + { + "Name": "Wallet2", + "ParticipationOnly": false + } + ] + } + ] +} diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/consensus-version-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/consensus-version-next-proto.diff new file mode 100644 index 0000000000..e9a2e42457 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/consensus-version-next-proto.diff @@ -0,0 +1,13 @@ +diff --git a/protocol/consensus.go b/protocol/consensus.go +index f81b9f7..7da20ac 100644 +--- a/protocol/consensus.go ++++ b/protocol/consensus.go +@@ -111,7 +111,7 @@ const ConsensusFuture = ConsensusVersion( + + // ConsensusCurrentVersion is the latest version and should be used + // when a specific version is not provided. +-const ConsensusCurrentVersion = ConsensusV17 ++const ConsensusCurrentVersion = ConsensusFuture + + // ConsensusTest0 is a version of ConsensusV0 used for testing + // (it has different approved upgrade paths). diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/enable-asset-create.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/enable-asset-create.diff new file mode 100644 index 0000000000..0cc1cdb3e5 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/enable-asset-create.diff @@ -0,0 +1,59 @@ +commit b90043d86d94b7a51af523430baf719ab72b0e00 +Author: Max Justicz +Date: Tue Oct 29 18:16:09 2019 -0400 + + Make asset name, URL, unit name fields variable length (#445) + + * make asset name, url, unit name fields variable length + + * fix error message + + * fix archival asset test + +diff --git a/libgoal/transactions.go b/libgoal/transactions.go +index 5349e79..32eb13b 100644 +--- a/libgoal/transactions.go ++++ b/libgoal/transactions.go +@@ -437,25 +437,36 @@ func (c *Client) MakeUnsignedAssetCreateTx(total uint64, defaultFrozen bool, man + } + } + +- if len(url) > len(tx.AssetParams.URL) { ++ // Get consensus params so we can get max field lengths ++ params, err := c.SuggestedParams() ++ if err != nil { ++ return transactions.Transaction{}, err ++ } ++ ++ cparams, ok := config.Consensus[protocol.ConsensusVersion(params.ConsensusVersion)] ++ if !ok { ++ return transactions.Transaction{}, errors.New("unknown consensus version") ++ } ++ ++ if len(url) > cparams.MaxAssetURLBytes { + return tx, fmt.Errorf("asset url %s is too long (max %d bytes)", url, len(tx.AssetParams.URL)) + } +- copy(tx.AssetParams.URL[:], []byte(url)) ++ tx.AssetParams.URL = url + + if len(metadataHash) > len(tx.AssetParams.MetadataHash) { + return tx, fmt.Errorf("asset metadata hash %x too long (max %d bytes)", metadataHash, len(tx.AssetParams.MetadataHash)) + } + copy(tx.AssetParams.MetadataHash[:], metadataHash) + +- if len(unitName) > len(tx.AssetParams.UnitName) { ++ if len(unitName) > cparams.MaxAssetUnitNameBytes { + return tx, fmt.Errorf("asset unit name %s too long (max %d bytes)", unitName, len(tx.AssetParams.UnitName)) + } +- copy(tx.AssetParams.UnitName[:], []byte(unitName)) ++ tx.AssetParams.UnitName = unitName + +- if len(assetName) > len(tx.AssetParams.AssetName) { ++ if len(assetName) > cparams.MaxAssetNameBytes { + return tx, fmt.Errorf("asset name %s too long (max %d bytes)", assetName, len(tx.AssetParams.AssetName)) + } +- copy(tx.AssetParams.AssetName[:], []byte(assetName)) ++ tx.AssetParams.AssetName = assetName + + return tx, nil + } diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/fast-upgrade-to-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/fast-upgrade-to-next-proto.diff new file mode 100644 index 0000000000..4f8e74f920 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/fast-upgrade-to-next-proto.diff @@ -0,0 +1,26 @@ +diff --git a/config/config.go b/config/config.go +index 4e69030..f4940ab 100644 +--- a/config/config.go ++++ b/config/config.go +@@ -410,6 +410,12 @@ func initConsensusProtocols() { + // ConsensusV17 points to 'final' spec commit + v17 := v16 + v17.ApprovedUpgrades = map[protocol.ConsensusVersion]bool{} ++ ++ v17.UpgradeVoteRounds = 2 ++ v17.UpgradeThreshold = 1 ++ v17.UpgradeWaitRounds = 2 ++ v17.ApprovedUpgrades[protocol.ConsensusFuture] = true ++ + Consensus[protocol.ConsensusV17] = v17 + + // v16 can be upgraded to v17. +@@ -427,7 +433,7 @@ func initConsensusProtocols() { + + // ConsensusFuture is used to test features that are implemented + // but not yet released in a production protocol version. +- vFuture := v18 ++ vFuture := v17 + vFuture.TxnCounter = true + vFuture.Asset = true + vFuture.LogicSigVersion = 1 diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/node-no-tx-broadcast.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/node-no-tx-broadcast.diff new file mode 100644 index 0000000000..da6ceacece --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/node-no-tx-broadcast.diff @@ -0,0 +1,37 @@ +diff --git a/node/node.go b/node/node.go +index a02828e..50095d4 100644 +--- a/node/node.go ++++ b/node/node.go +@@ -420,12 +420,12 @@ func (node *AlgorandFullNode) BroadcastSignedTxGroup(txgroup []transactions.Sign + enc = append(enc, protocol.Encode(tx)...) + txids = append(txids, tx.ID()) + } +- err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) +- if err != nil { +- node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) +- return err +- } +- node.log.Infof("Sent signed tx group with IDs %v", txids) ++ // err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) ++ // if err != nil { ++ // node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) ++ // return err ++ // } ++ // node.log.Infof("Sent signed tx group with IDs %v", txids) + return nil + } + +diff --git a/rpcs/txService.go b/rpcs/txService.go +index 7dec5e2..0f4ec6e 100644 +--- a/rpcs/txService.go ++++ b/rpcs/txService.go +@@ -50,7 +50,8 @@ type TxService struct { + responseSizeLimit int + } + +-const updateInterval = int64(30) ++// const updateInterval = int64(30) ++const updateInterval = int64(0) + const responseContentType = "application/x-algorand-ptx-v1" + + // calculate the number of bytes that would be consumed when packing a n-bytes buffer into a base64 buffer. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/txpool-remember.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/txpool-remember.diff new file mode 100644 index 0000000000..4a0c6f2c53 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/txpool-remember.diff @@ -0,0 +1,14 @@ +diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go +index 330bcfa..5c11929 100644 +--- a/data/pools/transactionPool.go ++++ b/data/pools/transactionPool.go +@@ -282,6 +282,9 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn) error { + } + + pool.rememberCommit(false) ++ ++ logging.Base().Infof("Transaction remembered %s", txgroup[0].ID()) ++ + return nil + } + diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/upgrade-path-to-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/upgrade-path-to-next-proto.diff new file mode 100644 index 0000000000..16a16b9f4e --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-1.1.304/upgrade-path-to-next-proto.diff @@ -0,0 +1,26 @@ +diff --git a/config/config.go b/config/config.go +index 4e69030..545a54b 100644 +--- a/config/config.go ++++ b/config/config.go +@@ -410,6 +410,12 @@ func initConsensusProtocols() { + // ConsensusV17 points to 'final' spec commit + v17 := v16 + v17.ApprovedUpgrades = map[protocol.ConsensusVersion]bool{} ++ ++ v17.UpgradeVoteRounds = 2 ++ v17.UpgradeThreshold = 1 ++ v17.UpgradeWaitRounds = 10000 ++ v17.ApprovedUpgrades[protocol.ConsensusFuture] = true ++ + Consensus[protocol.ConsensusV17] = v17 + + // v16 can be upgraded to v17. +@@ -427,7 +433,7 @@ func initConsensusProtocols() { + + // ConsensusFuture is used to test features that are implemented + // but not yet released in a production protocol version. +- vFuture := v18 ++ vFuture := v17 + vFuture.TxnCounter = true + vFuture.Asset = true + vFuture.LogicSigVersion = 1 diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/consensus-version-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/consensus-version-next-proto.diff new file mode 100644 index 0000000000..4a86b73a21 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/consensus-version-next-proto.diff @@ -0,0 +1,13 @@ +diff --git a/protocol/consensus.go b/protocol/consensus.go +index f81b9f7..7da20ac 100644 +--- a/protocol/consensus.go ++++ b/protocol/consensus.go +@@ -117,7 +117,7 @@ const ConsensusFuture = ConsensusVersion( + + // ConsensusCurrentVersion is the latest version and should be used + // when a specific version is not provided. +-const ConsensusCurrentVersion = ConsensusV19 ++const ConsensusCurrentVersion = ConsensusV19 + + // ConsensusTest0 is a version of ConsensusV0 used for testing + // (it has different approved upgrade paths). diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/fast-upgrade-to-next-proto b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/fast-upgrade-to-next-proto new file mode 100644 index 0000000000..5117b6341a --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/fast-upgrade-to-next-proto @@ -0,0 +1,16 @@ +diff --git a/config/config.go b/config/config.go +index 752bd4f..c6de011 100644 +--- a/config/config.go ++++ b/config/config.go +@@ -410,6 +410,11 @@ func initConsensusProtocols() { + // ConsensusV17 points to 'final' spec commit + v17 := v16 + v17.ApprovedUpgrades = map[protocol.ConsensusVersion]bool{} ++ ++ v17.UpgradeVoteRounds = 2 ++ v17.UpgradeThreshold = 1 ++ v17.UpgradeWaitRounds = 2 ++ + Consensus[protocol.ConsensusV17] = v17 + + // v16 can be upgraded to v17. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/node-no-tx-broadcast.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/node-no-tx-broadcast.diff new file mode 100644 index 0000000000..da6ceacece --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/node-no-tx-broadcast.diff @@ -0,0 +1,37 @@ +diff --git a/node/node.go b/node/node.go +index a02828e..50095d4 100644 +--- a/node/node.go ++++ b/node/node.go +@@ -420,12 +420,12 @@ func (node *AlgorandFullNode) BroadcastSignedTxGroup(txgroup []transactions.Sign + enc = append(enc, protocol.Encode(tx)...) + txids = append(txids, tx.ID()) + } +- err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) +- if err != nil { +- node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) +- return err +- } +- node.log.Infof("Sent signed tx group with IDs %v", txids) ++ // err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) ++ // if err != nil { ++ // node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) ++ // return err ++ // } ++ // node.log.Infof("Sent signed tx group with IDs %v", txids) + return nil + } + +diff --git a/rpcs/txService.go b/rpcs/txService.go +index 7dec5e2..0f4ec6e 100644 +--- a/rpcs/txService.go ++++ b/rpcs/txService.go +@@ -50,7 +50,8 @@ type TxService struct { + responseSizeLimit int + } + +-const updateInterval = int64(30) ++// const updateInterval = int64(30) ++const updateInterval = int64(0) + const responseContentType = "application/x-algorand-ptx-v1" + + // calculate the number of bytes that would be consumed when packing a n-bytes buffer into a base64 buffer. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/txpool-remember.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/txpool-remember.diff new file mode 100644 index 0000000000..4a0c6f2c53 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/txpool-remember.diff @@ -0,0 +1,14 @@ +diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go +index 330bcfa..5c11929 100644 +--- a/data/pools/transactionPool.go ++++ b/data/pools/transactionPool.go +@@ -282,6 +282,9 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn) error { + } + + pool.rememberCommit(false) ++ ++ logging.Base().Infof("Transaction remembered %s", txgroup[0].ID()) ++ + return nil + } + diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/upgrade-path-to-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/upgrade-path-to-next-proto.diff new file mode 100644 index 0000000000..6afef5e297 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.323/upgrade-path-to-next-proto.diff @@ -0,0 +1,16 @@ +diff --git a/config/config.go b/config/config.go +index 752bd4f..8bde27e 100644 +--- a/config/config.go ++++ b/config/config.go +@@ -410,6 +410,11 @@ func initConsensusProtocols() { + // ConsensusV17 points to 'final' spec commit + v17 := v16 + v17.ApprovedUpgrades = map[protocol.ConsensusVersion]bool{} ++ ++ v17.UpgradeVoteRounds = 2 ++ v17.UpgradeThreshold = 1 ++ v17.UpgradeWaitRounds = 10000 ++ + Consensus[protocol.ConsensusV17] = v17 + + // v16 can be upgraded to v17. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/node-no-tx-broadcast.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/node-no-tx-broadcast.diff new file mode 100644 index 0000000000..5f82e2596b --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/node-no-tx-broadcast.diff @@ -0,0 +1,29 @@ +diff --git a/node/node.go b/node/node.go +index 3c0f0c2..e84cc8a 100644 +--- a/node/node.go ++++ b/node/node.go +@@ -450,8 +450,8 @@ func (node *AlgorandFullNode) BroadcastSignedTxn(signed transactions.SignedTxn) + return transactions.Txid{}, err + } + +- node.net.Broadcast(context.TODO(), protocol.TxnTag, protocol.Encode(signed), true, nil) +- node.log.Infof("Sent signed tx %s", signed.ID()) ++ // node.net.Broadcast(context.TODO(), protocol.TxnTag, protocol.Encode(signed), true, nil) ++ // node.log.Infof("Sent signed tx %s", signed.ID()) + return signed.ID(), nil + } + +diff --git a/rpcs/txService.go b/rpcs/txService.go +index c117cf5..4b25782 100644 +--- a/rpcs/txService.go ++++ b/rpcs/txService.go +@@ -50,7 +50,8 @@ type TxService struct { + responseSizeLimit int + } + +-const updateInterval = int64(30) ++// const updateInterval = int64(30) ++const updateInterval = int64(0) + const responseContentType = "application/x-algorand-ptx-v1" + + // calculate the number of bytes that would be consumed when packing a n-bytes buffer into a base64 buffer. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/txpool-remember.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/txpool-remember.diff new file mode 100644 index 0000000000..f39c128703 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/stable-1.0.29/txpool-remember.diff @@ -0,0 +1,13 @@ +diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go +index 79aa4a9..8ad4f93 100644 +--- a/data/pools/transactionPool.go ++++ b/data/pools/transactionPool.go +@@ -233,6 +233,8 @@ func (pool *TransactionPool) Remember(t transactions.SignedTxn) error { + // last, update the spent algos from the sender account + pool.algosPendingSpend.accountForTransactionDeductions(t.Txn, deductions) + ++ logging.Base().Infof("Transaction remembered %s", t.ID()) ++ + return nil + } + From 502e05b6af4b53c69ac68c2ab84997ff38d7ca87 Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Mon, 3 Aug 2020 13:39:33 -0400 Subject: [PATCH 258/267] Testing script for Gossip and TxSync for Algorand Aug-2020 release --- .../gossip-txsync-upgrade/README.md | 102 +++++++ .../gossip-txsync-upgrade/functional-test.sh | 267 +++++++++--------- .../network-config/three-nodes.json | 29 +- .../consensus-version-next-proto.diff | 13 + .../fast-upgrade-to-next-proto.diff | 32 +++ .../nightly-2.0.574/node-no-tx-broadcast.diff | 37 +++ .../rel/nightly-2.0.574/txpool-remember.diff | 13 + .../upgrade-path-to-next-proto.diff | 32 +++ .../v2.0.6-stable/node-no-tx-broadcast.diff | 37 +++ .../patch/v2.0.6-stable/txpool-remember.diff | 14 + 10 files changed, 448 insertions(+), 128 deletions(-) create mode 100644 test/release-testing/gossip-txsync-upgrade/README.md create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/consensus-version-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/fast-upgrade-to-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/node-no-tx-broadcast.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/txpool-remember.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/upgrade-path-to-next-proto.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/node-no-tx-broadcast.diff create mode 100644 test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/txpool-remember.diff diff --git a/test/release-testing/gossip-txsync-upgrade/README.md b/test/release-testing/gossip-txsync-upgrade/README.md new file mode 100644 index 0000000000..b6741328b6 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/README.md @@ -0,0 +1,102 @@ +# Differential test for txn propagation test over Gossip and TxSync + +## Overview + +The test checks behavior of old (prev release) and new (release candidate) binaries in the following scenarios: +1. Pre upgraded network (old and new binaries) +2. Upgrade proposed but not applied (old and new binaries) +3. Upgraded network (new binaries) +4. Upgrade and upgrade+1 round (new binaries) + +Checks: +* Gossip and TxSync work as expected +* All basic txn (txn types/fields valid in prev release) valid for both old and new binaries +* New txn (txn types/fields valid only in release candidate) are rejected by old binary and before the upgrade +* Submitting transactions during upgrade works as expected + - submit at upgrade round + - txn land into next block and they are valid at upgrade+1 round + +## How it works + +The script creates a network and then rebuilds binaries to generate all txn needed in tests: +* Basic transactions +* New transactions + +The generation happens on the same network so that the network is upgraded during generation phase. At the end there are basic txn generated by new and old binaries, and new txns generated by new binary that are valid on the same network. + +Then the script rebuild binaries with either Gossip or TxSync enabled and runs pre-generated transactions. At each iteration it compares + +### Building binaries and Patches + +The script requires a path to `go-algorand` repo to checkout required version, patch and rebuild. +Patches per version are stored under `patch` directory in according to **git tag** names. + +#### Patches for old version + +1. Disable txn broadcast in `node-no-tx-broadcast.diff` file +2. Logging a fact txn landed into transaction pool in `txpool-remember.diff` file + +#### Patches for new version + +1. Same as for before for broadcast disabling and tnx logging +2. Optional `consensus-version-next-proto.diff` patch to set `ConsensusCurrentVersion` to **Future** +3. Upgrade path `upgrade-path-to-next-proto.diff` to a new protocol version +4. Upgrade path and fast upgrade `fast-upgrade-to-next-proto.diff` to a new protocol version + +### Transaction generation + +There are two related functions: `generate_transactions` and `check_new_transactions`. Both have a trigger for new txn generation. +```bash + if version_gt "$alogd_version" "$BASE_VERSION" ; then + if [ "$proto" == "$NEXT_PROTO" ]; then + # ... + fi + fi +``` + +The first one `generate_transactions` only _generates_ and saves txn, but the second `check_new_transactions` sends transactions to the network. This is useful for the scenario when subsequent transaction depends on previous: **asset create** txn can be run at upgrade round but **asset config** requires the asset to exist, so that it can only be executed at next round. +If there is no such dependencies then `check_new_transactions` can be simply omitted. + +## How to run + +1. Define versions and ensure git tags exist: + ```bash + BASE_VERSION="2.0.6" + RELEASE_V2="v${BASE_VERSION}-stable" + + CURRENT_VERSION="2.0.574" + CURRENT="rel/nightly-${CURRENT_VERSION}" + + # git revision of the previous release + STABLE=$RELEASE_V2 + # git revision of the current (new) release + TESTING=$CURRENT + ``` + Base is a prev release, current is a new version. +2. Define protocols: + ```bash + V23="https://github.com/algorandfoundation/specs/tree/e5f565421d720c6f75cdd186f7098495caf9101f" + VFU="future" + + BASE_PROTO=$V23 + NEXT_PROTO=$VFU + ``` +3. Create patches. See previous section and existing patches for reference. +4. Set `init_generate_network_and_txn` parameter and run the script to generate network and transactions. +5. Disable `init_generate_network_and_txn` and revisit txn counters: + ```bash + # payset, keyreg, lsig, group, asset + TXN_BASE_COUNT=5 + # base + app call, rekey, rekeyed + TXN_ALL_COUNT=8 + ``` +6. Use test switches for tests selection + ```bash + test_new_pre_upgrade="" + test_old_pre_upgrade="" + test_new_upgrade_proposed="" + test_new_upgrade_applied="" + test_at_upgrade_round="" + ``` + +The script prints out errors if any. diff --git a/test/release-testing/gossip-txsync-upgrade/functional-test.sh b/test/release-testing/gossip-txsync-upgrade/functional-test.sh index d8fc375933..2153c982c0 100755 --- a/test/release-testing/gossip-txsync-upgrade/functional-test.sh +++ b/test/release-testing/gossip-txsync-upgrade/functional-test.sh @@ -13,22 +13,36 @@ fi BASE_VERSION="1.0.29" RELEASE_V1="rel/stable-${BASE_VERSION}" -CURRENT_VERSION="1.1.304" +BASE_VERSION="2.0.6" +RELEASE_V2="v${BASE_VERSION}-stable" + CURRENT_VERSION="2.0.323" CURRENT="rel/nightly-${CURRENT_VERSION}" +# after build 399 there is no nightly tags +# use the code below to get the latest nightly build version and create a branch +<< COMMENT +COMMIT=$(git log upstream/rel/nightly -1 --format=oneline -- buildnumber.dat | cut -d' ' -f 1-1) +BUILD=$(echo $COMMIT | xargs git show $1 --format=oneline | tail -1 | cut -c 2-) +CURRENT_VERSION="2.0.$BUILD" +git checkout $COMMIT -b rel/nightly-$CURRENT_VERSION +COMMENT +CURRENT_VERSION="2.0.574" +CURRENT="rel/nightly-${CURRENT_VERSION}" # Parameters # git revision of the previous release -STABLE=$RELEASE_V1 +STABLE=$RELEASE_V2 # git revision of the current (new) release TESTING=$CURRENT # Protocol versions V17="https://github.com/algorandfoundation/specs/tree/5615adc36bad610c7f165fa2967f4ecfa75125f0" V19="https://github.com/algorandfoundation/specs/tree/03ae4eac54f1325377d0a2df62b5ef7cc08c5e18" +V23="https://github.com/algorandfoundation/specs/tree/e5f565421d720c6f75cdd186f7098495caf9101f" +VFU="future" -BASE_PROTO=$V17 -NEXT_PROTO=$V19 +BASE_PROTO=$V23 +NEXT_PROTO=$VFU # if testing against stable-1.0.29 release, ensure go-algorand is in $GOPATH/src/github.com/algorand if [ "${STABLE}" = "${RELEASE_V1}" ]; then @@ -181,7 +195,8 @@ function build_algorand_by_rev() { mkdir -p "$target_dir" # build - make install > "$target_dir/build.log" 2>&1 + git diff > "$target_dir/build.log" + make install >> "$target_dir/build.log" 2>&1 trap - ERR @@ -190,7 +205,7 @@ function build_algorand_by_rev() { popd - bin_files=("algod" "carpenter" "goal" "kmd" "msgpacktool") + bin_files=("algod" "carpenter" "goal" "kmd" "msgpacktool" "algokey") for bin in "${bin_files[@]}"; do cp "${GO_BIN}/${bin}" $target_dir if [ $? -ne 0 ]; then exit 1; fi @@ -271,8 +286,8 @@ DESCRIPTION function update_network_node_config_for_gossip() { update_network_node_config "$network_dir" TxSyncIntervalSeconds 3600 update_network_node_config "$network_dir" BaseLoggerDebugLevel 5 - update_network_node_config "$network_dir" IncomingConnectionsLimit -1 - update_network_node_config "$network_dir" Version 4 + update_network_node_config "$network_dir" IncomingConnectionsLimit 10240 + update_network_node_config "$network_dir" Version 6 } << DESCRIPTION @@ -283,8 +298,8 @@ DESCRIPTION function update_network_node_config_for_txsync() { update_network_node_config "$network_dir" TxSyncIntervalSeconds 1 update_network_node_config "$network_dir" BaseLoggerDebugLevel 5 - update_network_node_config "$network_dir" IncomingConnectionsLimit -1 - update_network_node_config "$network_dir" Version 4 + update_network_node_config "$network_dir" IncomingConnectionsLimit 10240 + update_network_node_config "$network_dir" Version 6 } function fresh_temp_net() { @@ -962,7 +977,7 @@ Parameters: emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations network_dir - a path to the directory with running network tx_dir - a path to the directory with pre-generated transactions to submit - proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit asset transaction + proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit new transaction Returns: 0 on success __submit_and_check is set to amount of txsync requests found in the log @@ -1026,9 +1041,9 @@ function submit_and_check() { fi done - check_asset_transactions "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$proto" - local asset_txids=( ${__check_asset_transactions[@]} ) - local txids=( "${txids[@]}" "${asset_txids[@]}" ) + # check_new_transactions "$(echo ${nodes[@]})" "$(echo ${emit_info[@]})" "$network_dir" "$proto" + # local new_txids=( ${__check_new_transactions[@]} ) + # local txids=( "${txids[@]}" "${new_txids[@]}" ) # wait to let all tx get propagated "$bin_dir/goal" node wait -w 30 -d "$network_dir/$sender_name" @@ -1092,8 +1107,12 @@ function generate_transactions() { trap "network_cleanup $bin_dir $network_dir" ERR + # echo "To stop type" + # echo "$bin_dir/goal" network stop -r "$network_dir" + # echo "$bin_dir/goal" network delete -r "$network_dir" + "$bin_dir/goal" network start -r "$network_dir" - "$bin_dir/goal" node wait -w 30 -d "$network_dir/Primary" + "$bin_dir/goal" node wait -w 60 -d "$network_dir/Primary" if [ "$proto" == "$NEXT_PROTO" ]; then local cmd="$bin_dir/goal node status -d $network_dir/$sender_name" @@ -1129,27 +1148,42 @@ function generate_transactions() { "$bin_dir/goal" account changeonlinestatus --fee $fee --address $src_addr -o -t "$tx_dir/keyreg.tx" --firstRound "$firstvalid" --validRounds "$validrounds" -d "$network_dir/$sender_name" "$bin_dir/goal" clerk sign -i "$tx_dir/keyreg.tx" -o "$tx_dir/keyreg.stx" -d "$network_dir/$sender_name" + trace_if_needed "Creating logic sig payset tx" + fee=$(get_random_fee) + echo "int 1" > "$tx_dir/int1.teal" + "$bin_dir/goal" clerk compile "$tx_dir/int1.teal" -s -a $src_addr -o "$tx_dir/int1.lsig" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-teal.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -L "$tx_dir/int1.lsig" -i "$tx_dir/payset-teal.tx" -o "$tx_dir/payset-teal.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating payset group tx" + fee=$(get_random_fee) + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-again.tx" -d "$network_dir/$sender_name" + cat "$tx_dir/payset.tx" "$tx_dir/payset-again.tx" > "$tx_dir/payset-concat.tx" + "$bin_dir/goal" clerk group -i "$tx_dir/payset-concat.tx" -o "$tx_dir/payset-group.tx" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/payset-group.tx" -o "$tx_dir/payset-group.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating asset create tx" + fee=$(get_random_fee) + "$bin_dir/goal" asset create --fee $fee --creator $src_addr --name $ASSET_NAME --total 100 --unitname $ASSET_TOKEN --firstvalid "$firstvalid" --validrounds "$validrounds" -o "$tx_dir/asset-create.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/asset-create.tx" -o "$tx_dir/asset-create.stx" -d "$network_dir/$sender_name" + if version_gt "$alogd_version" "$BASE_VERSION" ; then if [ "$proto" == "$NEXT_PROTO" ]; then - trace_if_needed "Creating logic sig payset tx" - fee=$(get_random_fee) - echo "int 1" > "$tx_dir/int1.teal" - # local escrow_addr="$("$bin_dir/goal" clerk compile "$tx_dir/int1.teal" | cut -f 2 -d ':' | sed -e 's/[[:space:]]//g')" - "$bin_dir/goal" clerk compile "$tx_dir/int1.teal" -s -a $src_addr -o "$tx_dir/int1.lsig" -d "$network_dir/$sender_name" - "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-teal.tx" -d "$network_dir/$sender_name" - "$bin_dir/goal" clerk sign -P "$proto" -L "$tx_dir/int1.lsig" -i "$tx_dir/payset-teal.tx" -o "$tx_dir/payset-teal.stx" -d "$network_dir/$sender_name" - - trace_if_needed "Creating payset group tx" - fee=$(get_random_fee) - "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/payset-again.tx" -d "$network_dir/$sender_name" - cat "$tx_dir/payset.tx" "$tx_dir/payset-again.tx" > "$tx_dir/payset-concat.tx" - "$bin_dir/goal" clerk group -i "$tx_dir/payset-concat.tx" -o "$tx_dir/payset-group.tx" - "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/payset-group.tx" -o "$tx_dir/payset-group.stx" -d "$network_dir/$sender_name" - - trace_if_needed "Creating asset create tx" - fee=$(get_random_fee) - "$bin_dir/goal" asset create --fee $fee --creator $src_addr --name $ASSET_NAME --total 100 --unitname $ASSET_TOKEN --firstvalid "$firstvalid" --validrounds "$validrounds" -o "$tx_dir/asset-create.tx" -d "$network_dir/$sender_name" - "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/asset-create.tx" -o "$tx_dir/asset-create.stx" -d "$network_dir/$sender_name" + trace_if_needed "Creating app create tx" + printf "#pragma version 2\nint 1" > "$tx_dir/int1.teal" + "$bin_dir/goal" app create --creator $src_addr --approval-prog "$tx_dir/int1.teal" --clear-prog "$tx_dir/int1.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --firstvalid "$firstvalid" --lastvalid "$lastvalid" -o "$tx_dir/app-create.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/app-create.tx" -o "$tx_dir/app-create.stx" -d "$network_dir/$sender_name" + + local rekey_src_addr=$("$bin_dir/goal" account list -d "$network_dir/$sender_name" | tail -1 | awk '{ print $2 }') + local rekey_to_addr=$src_addr + + trace_if_needed "Creating rekey txn" + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $rekey_src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" --rekey-to $rekey_to_addr -o "$tx_dir/rekey-payset-1.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -P "$proto" -i "$tx_dir/rekey-payset-1.tx" -o "$tx_dir/rekey-payset-1.stx" -d "$network_dir/$sender_name" + + trace_if_needed "Creating rekeyed txn that also rekeys it back" + "$bin_dir/goal" clerk send --fee $fee -a 1000 -f $rekey_src_addr -t $dst_addr --firstvalid "$firstvalid" --lastvalid "$lastvalid" --rekey-to $rekey_src_addr -o "$tx_dir/rekey-payset-2.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -S $rekey_to_addr -P "$proto" -i "$tx_dir/rekey-payset-2.tx" -o "$tx_dir/rekey-payset-2.stx" -d "$network_dir/$sender_name" fi fi @@ -1157,21 +1191,21 @@ function generate_transactions() { network_cleanup "$bin_dir" "$network_dir" } -__check_asset_transactions=() +__check_new_transactions=() << DESCRIPTION -Submits asset transactions +Submits transactions of new type - that are only valid after upgrade Parameters: nodes - an array (encoded map) of node names and binary paths emit_info - an array (encoded map) with sender, receiver and update initiator nodes names and tx count expectations network_dir - a path to the directory with running network - proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit asset transaction + proto - a protocol version is being test. If it is '$NEXT_PROTO' then submit new transaction Returns: 0 on success - __check_asset_transactions contains an array of submitted tx ids + __check_new_transactions contains an array of submitted tx ids DESCRIPTION -function check_asset_transactions() { +function check_new_transactions() { local nodes=( $1 ) local emit_info=( $2 ) local network_dir=$3 @@ -1222,39 +1256,36 @@ function check_asset_transactions() { "$bin_dir/goal" node wait -w 30 -d "$network_dir/$sender_name" "$bin_dir/goal" node wait -w 30 -d "$network_dir/$receiver_name" + local rekey_src_addr=$("$bin_dir/goal" account list -d "$network_dir/$sender_name" | tail -1 | awk '{ print $2 }') + local rekey_to_addr=$src_addr + local regex='txid ([A-Z0-9=]{52,52})' # Issued transaction from account AKMIEYU64TDLTDER6LTGPRMAXUMDQQFKVMH5QTYECBWIXT7V4KZ6GFTI2I, txid ITJPKM7JW57HTJUWIZH3VDUYJ6VVAHPASMKGKIFXZT2P6M3IEKAA (fee 1000) - trace_if_needed "Sending asset config tx" - local output=$("$bin_dir/goal" asset config --asset $ASSET_TOKEN --creator $src_addr --manager $src_addr --new-manager $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") - trace_if_needed "Sent asset config tx" "$output" $? + printf "#pragma version 2\nint 1" > "$tx_dir/int1.teal" + trace_if_needed "Sending app create tx" + local output=$("$bin_dir/goal" app create --creator $src_addr --approval-prog "$tx_dir/int1.teal" --clear-prog "$tx_dir/int1.teal" --global-byteslices 0 --global-ints 0 --local-byteslices 0 --local-ints 0 --firstvalid 1 --lastvalid 1000 -d "$network_dir/$sender_name") + trace_if_needed "Sent app create tx" "$output" $? [[ $output =~ $regex ]] local txid=${BASH_REMATCH[1]} trace_if_needed $txid txids+=($txid) - trace_if_needed "Sending asset send tx" - local output=$("$bin_dir/goal" asset send --asset $ASSET_TOKEN --creator $src_addr -a 1 -f $src_addr -t $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") - trace_if_needed "Sent asset send tx" "$output" $? + trace_if_needed "Sending rekey txn" + local output=$("$bin_dir/goal" clerk send --fee 1000 -a 1000 -f $rekey_src_addr -t $dst_addr --firstvalid 1 --lastvalid 1000 --rekey-to $rekey_to_addr -d "$network_dir/$sender_name") + trace_if_needed "Sent rekey tx" "$output" $? [[ $output =~ $regex ]] local txid=${BASH_REMATCH[1]} trace_if_needed $txid txids+=($txid) - trace_if_needed "Sending asset freeze tx" - local output=$("$bin_dir/goal" asset freeze --asset $ASSET_TOKEN --creator $src_addr --freezer $src_addr --account $src_addr -N --freeze --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") - trace_if_needed "Sent asset freeze tx" "$output" $? - - [[ $output =~ $regex ]] - local txid=${BASH_REMATCH[1]} - trace_if_needed $txid - txids+=($txid) - - trace_if_needed "Sending asset destroy tx" - local output=$("$bin_dir/goal" asset destroy --asset $ASSET_TOKEN --creator $src_addr --manager $src_addr -N --firstvalid 1 --validrounds 1000 -d "$network_dir/$sender_name") - trace_if_needed "Sent asset destroy tx" "$output" $? + trace_if_needed "Sending rekeyed txn" + mkdir -p "$tx_dir/tmp" + "$bin_dir/goal" clerk send --fee 1000 -a 1000 -f $rekey_src_addr -t $dst_addr --firstvalid 1 --lastvalid 1000 --rekey-to $rekey_src_addr -o "$tx_dir/tmp/rekeyed-send-payset.tx" -d "$network_dir/$sender_name" + "$bin_dir/goal" clerk sign -S $rekey_to_addr -P "$proto" -i "$tx_dir/tmp/rekeyed-send-payset.tx" -o "$tx_dir/tmp/rekeyed-send-payset.stx" -d "$network_dir/$sender_name" + local output=$("$bin_dir/goal" clerk rawsend -f "$tx_dir/tmp/rekeyed-send-payset.stx") -d "$network_dir/$sender_name" [[ $output =~ $regex ]] local txid=${BASH_REMATCH[1]} @@ -1262,7 +1293,7 @@ function check_asset_transactions() { txids+=($txid) fi - __check_asset_transactions=( ${txids[@]} ) + __check_new_transactions=( ${txids[@]} ) } TESTING_BIN_DIR="${SCRIPTPATH}/tests/$(revision_to_name $TESTING)/bin" @@ -1283,16 +1314,26 @@ if [ ! -d "$VANILLA_STABLE_BIN_DIR" ]; then build_binaries "$SRC_DIR" "$STABLE" "$VANILLA_STABLE_BIN_DIR" fi +# flag indicating the need of txn generation +# usually it is done only once at the beginning +init_generate_network_and_txn="" + +# payset, keyreg, lsig, group, asset +TXN_BASE_COUNT=5 +# base + app call, rekey, rekeyed +TXN_ALL_COUNT=8 + +# test flags - see details at each test below test_new_pre_upgrade="" test_old_pre_upgrade="" test_new_upgrade_proposed="" -test_new_upgrade_applied="1" -test_at_upgrade_round="1" -test_next_proto_standalone="1" -test_old_node_after_upgrade="" +test_new_upgrade_applied="" +test_at_upgrade_round="" +test_next_proto_standalone="" # !!! <-- this test breaks all pre-generated transactions !!! +if [ -n "$init_generate_network_and_txn" ]; then # generate sample network once to have the same genesis for all subsequent tests (except the separate future test) -generate_network "$VANILLA_TESTING_BIN_DIR" "$V17" +generate_network "$VANILLA_TESTING_BIN_DIR" "$BASE_PROTO" # pre-generate old transactions SENDER="Node1" @@ -1304,12 +1345,12 @@ generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" # generate tx with valid round=UPGRADE_ROUND for at upgrade test TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND" trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND" -generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$BASE_PROTO" "$UPGRADE_ROUND" # generate tx with valid round=UPGRADE_ROUND+1 for at upgrade+1 test TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND_NEXT" trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE transaction with validity round=$UPGRADE_ROUND_NEXT" -generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND_NEXT" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$BASE_PROTO" "$UPGRADE_ROUND_NEXT" SENDER="Primary" RECEIVER="Node2" @@ -1320,12 +1361,12 @@ generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" # generate tx with valid round=UPGRADE_ROUND for at upgrade test TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND" trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND" -generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$BASE_PROTO" "$UPGRADE_ROUND" # generate tx with valid round=UPGRADE_ROUND+1 for at upgrade test TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)/$SENDER/Round$UPGRADE_ROUND_NEXT" trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $STABLE with validity round=$UPGRADE_ROUND_NEXT" -generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$V17" "$UPGRADE_ROUND_NEXT" +generate_transactions "$VANILLA_STABLE_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$BASE_PROTO" "$UPGRADE_ROUND_NEXT" SENDER="Node2" RECEIVER="Primary" @@ -1373,12 +1414,17 @@ RECEIVER="Primary" TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER" trace_if_needed "Creating tx $SENDER -> $RECEIVER by version $TESTING transaction" generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_PROTO" +fi +#################################################################################################### +# # Tests # Note set -e handles exit after each failed tests +# +#################################################################################################### if [ -n "$test_new_pre_upgrade" ]; then echo "=============================================================================================" -echo "1. Check v1.1.0 accepts old and new basic transactions (pre upgrade)" +echo "1. Check NEW ALGOD accepts old and new basic transactions (pre upgrade)" echo NODES=( @@ -1386,7 +1432,7 @@ NODES=( "Node1:$TESTING_BIN_DIR" "Node2:$TESTING_BIN_DIR" ) -COUNT=2 +COUNT=$TXN_BASE_COUNT echo "---------------------------------------------------------------------------------------------" echo "1.1. TxSync disabled" @@ -1437,7 +1483,7 @@ fi if [ -n "$test_old_pre_upgrade" ]; then echo "=============================================================================================" -echo "2. Check v1.0.29 accepts old and new basic transactions (pre upgrade)" +echo "2. Check OLD ALGOD accepts old and new basic transactions (pre upgrade)" echo NODES=( @@ -1445,7 +1491,7 @@ NODES=( "Node1:$STABLE_BIN_DIR" "Node2:$STABLE_BIN_DIR" ) -COUNT=2 +COUNT=$TXN_BASE_COUNT echo "---------------------------------------------------------------------------------------------" echo "2.1. TxSync disabled" @@ -1496,7 +1542,7 @@ fi if [ -n "$test_new_upgrade_proposed" ]; then echo "=============================================================================================" -echo "3. Check v1.1.0 and v1.0.29 accept old and new basic transactions (upgrade proposed)" +echo "3. Check NEW algod and OLD algod accept old and new basic transactions (upgrade proposed)" echo NODES=( @@ -1504,7 +1550,7 @@ NODES=( "Node1:$TESTING_BIN_DIR" "Node2:$STABLE_BIN_DIR" ) -COUNT=2 +COUNT=$TXN_BASE_COUNT PROPOSER="Node1" echo "---------------------------------------------------------------------------------------------" @@ -1594,7 +1640,7 @@ fi if [ -n "$test_new_upgrade_applied" ]; then echo "=============================================================================================" -echo "4. Check v1.1.0 accepts all old and new transactions (post upgrade)" +echo "4. Check NEW ALGOD accepts old basic and all new transactions (post upgrade)" echo NODES=( @@ -1602,7 +1648,8 @@ NODES=( "Node1:$TESTING_BIN_DIR" "Node2:$TESTING_BIN_DIR" ) -COUNT=9 +COUNT=$TXN_ALL_COUNT +OLD_COUNT=$TXN_BASE_COUNT echo "---------------------------------------------------------------------------------------------" echo "4.1. TxSync disabled" @@ -1625,8 +1672,8 @@ test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX # test new binaries with old transactions EMIT_INFO=( - "snd:$SENDER@2" - "rcv:$RECEIVER@2" + "snd:$SENDER@$OLD_COUNT" + "rcv:$RECEIVER@$OLD_COUNT" ) TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" @@ -1652,8 +1699,8 @@ test_upgrade_applied_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX # test new binaries with old transactions EMIT_INFO=( - "snd:$SENDER@2" - "rcv:$RECEIVER@2" + "snd:$SENDER@$OLD_COUNT" + "rcv:$RECEIVER@$OLD_COUNT" ) TX_DIR="$TX_BASE_DIR/$(revision_to_name $STABLE)" test_upgrade_applied_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" @@ -1661,7 +1708,7 @@ fi if [ -n "$test_at_upgrade_round" ]; then echo "=============================================================================================" -echo "5. Check v1.1.0 accepts all old and new transactions (upgrade and upgrade+1)" +echo "5. Check NEW algod accepts all old and new transactions (upgrade and upgrade+1)" echo NODES=( @@ -1669,7 +1716,7 @@ NODES=( "Node1:$TESTING_BIN_DIR" "Node2:$TESTING_BIN_DIR" ) -COUNT=2 +COUNT=$TXN_BASE_COUNT ((SUBMIT_ROUND=$UPGRADE_ROUND-1)) echo "---------------------------------------------------------------------------------------------" @@ -1688,7 +1735,7 @@ trace_if_needed "check upgrade round" ((TX_VALID_ROUND=$UPGRADE_ROUND)) trace_if_needed "test new binaries with old transactions" -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1697,7 +1744,7 @@ TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" trace_if_needed "test new binaries with old transactions" -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1710,8 +1757,8 @@ trace_if_needed "check upgrade+1 round" SUBMIT_ROUND="$UPGRADE_ROUND" ((TX_VALID_ROUND=$UPGRADE_ROUND+1)) -trace_if_needed "test new binaries with old transactions" -COUNT=9 +trace_if_needed "test new binaries with all transactions" +COUNT=$TXN_ALL_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1720,7 +1767,7 @@ TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" test_at_upgrade_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" trace_if_needed "test new binaries with old transactions" -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1743,7 +1790,7 @@ trace_if_needed "check upgrade round" ((SUBMIT_ROUND=$UPGRADE_ROUND-1)) ((TX_VALID_ROUND=$UPGRADE_ROUND)) # test new binaries with old transactions -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1752,7 +1799,7 @@ TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" trace_if_needed "test new binaries with old transactions" -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1765,8 +1812,8 @@ trace_if_needed "check upgrade+1 round" SUBMIT_ROUND="$UPGRADE_ROUND" ((TX_VALID_ROUND=$UPGRADE_ROUND+1)) -trace_if_needed "test new binaries with old transactions" -COUNT=9 +trace_if_needed "test new binaries with all transactions" +COUNT=$TXN_ALL_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1775,7 +1822,7 @@ TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)/$SENDER/Round$TX_VALID_ROUND" test_at_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$UPGRADE_ROUND" "$SUBMIT_ROUND" trace_if_needed "test new binaries with old transactions" -COUNT=2 +COUNT=$TXN_BASE_COUNT EMIT_INFO=( "snd:$SENDER@$COUNT" "rcv:$RECEIVER@$COUNT" @@ -1786,7 +1833,7 @@ fi if [ -n "$test_next_proto_standalone" ]; then echo "=============================================================================================" -echo "6. Check v1.1.0 accepts all transactions (create a fresh v=NEXT_PROTO network)" +echo "6. Check NEW algod accepts all transactions (create a fresh v=NEXT_PROTO network)" echo "This test re-generates the network so that no pre-generated transactions are valid" NODES=( @@ -1794,7 +1841,7 @@ NODES=( "Node1:$TESTING_BIN_DIR" "Node2:$TESTING_BIN_DIR" ) -COUNT=9 +COUNT=$TXN_ALL_COUNT generate_network "$VANILLA_TESTING_BIN_DIR" "$NEXT_PROTO" @@ -1838,37 +1885,3 @@ generate_transactions "$TESTING_BIN_DIR" "$SENDER" "$RECEIVER" "$TX_DIR" "$NEXT_ build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_disable_gossip test_pre_upgrade_txsync "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" "$NEXT_PROTO" fi - - -if [ -n "$test_old_node_after_upgrade" ]; then -echo "=============================================================================================" -echo "7. Check v1.0.29 does not crash after upgrade (post upgrade)" -echo - -NODES=( - "Primary:$TESTING_BIN_DIR" - "Node1:$TESTING_BIN_DIR" - "Node2:$STABLE_BIN_DIR" -) -COUNT=9 - -echo "---------------------------------------------------------------------------------------------" -echo "7.1. TxSync disabled" - -SENDER="Node1" -RECEIVER="Primary" -echo "Will be sending from $SENDER to $RECEIVER" -echo - -EMIT_INFO=( - "snd:$SENDER@$COUNT" - "rcv:$RECEIVER@$COUNT" -) - -# build_binaries "$SRC_DIR" "$STABLE" "$STABLE_BIN_DIR" patch_log_txpool_remember -# build_binaries "$SRC_DIR" "$TESTING" "$TESTING_BIN_DIR" patch_agg_remember_and_fast_upgrade_to_next_proto - -# test new binaries with new transactions -TX_DIR="$TX_BASE_DIR/$(revision_to_name $TESTING)" -test_upgrade_applied_gossip "$(echo ${NODES[@]})" "$(echo ${EMIT_INFO[@]})" "$TX_DIR" -fi diff --git a/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json b/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json index dad8b0021a..4ccc4fc076 100644 --- a/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json +++ b/test/release-testing/gossip-txsync-upgrade/network-config/three-nodes.json @@ -7,15 +7,30 @@ "Stake": 1, "Online": true }, + { + "Name": "WalletPR", + "Stake": 1, + "Online": true + }, { "Name": "Wallet1", - "Stake": 98, + "Stake": 95, + "Online": true + }, + { + "Name": "Wallet1R", + "Stake": 1, "Online": true }, { "Name": "Wallet2", "Stake": 1, "Online": true + }, + { + "Name": "Wallet2R", + "Stake": 1, + "Online": true } ] }, @@ -27,6 +42,10 @@ { "Name": "WalletP", "ParticipationOnly": false + }, + { + "Name": "WalletPR", + "ParticipationOnly": false } ] }, @@ -37,6 +56,10 @@ { "Name": "Wallet1", "ParticipationOnly": false + }, + { + "Name": "Wallet1R", + "ParticipationOnly": false } ] }, @@ -47,6 +70,10 @@ { "Name": "Wallet2", "ParticipationOnly": false + }, + { + "Name": "Wallet2R", + "ParticipationOnly": false } ] } diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/consensus-version-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/consensus-version-next-proto.diff new file mode 100644 index 0000000000..9b778b7a93 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/consensus-version-next-proto.diff @@ -0,0 +1,13 @@ +diff --git a/protocol/consensus.go b/protocol/consensus.go +index 86928db4..926f7a0c 100644 +--- a/protocol/consensus.go ++++ b/protocol/consensus.go +@@ -140,7 +140,7 @@ const ConsensusFuture = ConsensusVersion( + + // ConsensusCurrentVersion is the latest version and should be used + // when a specific version is not provided. +-const ConsensusCurrentVersion = ConsensusV23 ++const ConsensusCurrentVersion = ConsensusFuture + + // Error is used to indicate that an unsupported protocol has been detected. + type Error ConsensusVersion diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/fast-upgrade-to-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/fast-upgrade-to-next-proto.diff new file mode 100644 index 0000000000..cbab890fd6 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/fast-upgrade-to-next-proto.diff @@ -0,0 +1,32 @@ +diff --git a/config/consensus.go b/config/consensus.go +index e9892d85..c55be3dd 100644 +--- a/config/consensus.go ++++ b/config/consensus.go +@@ -495,7 +495,7 @@ func initConsensusProtocols() { + + MaxBalLookback: 320, + +- MaxTxGroupSize: 1, ++ MaxTxGroupSize: 1, + } + + v7.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} +@@ -680,12 +680,18 @@ func initConsensusProtocols() { + v23 := v22 + v23.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + v23.FixTransactionLeases = true ++ v23.UpgradeVoteRounds = 2 ++ v23.UpgradeThreshold = 1 ++ v23.MinUpgradeWaitRounds = 0 ++ v23.MaxUpgradeWaitRounds = 5 + Consensus[protocol.ConsensusV23] = v23 + // v22 can be upgraded to v23. + v22.ApprovedUpgrades[protocol.ConsensusV23] = 10000 + // v21 can be upgraded to v23. + v21.ApprovedUpgrades[protocol.ConsensusV23] = 0 + ++ v23.ApprovedUpgrades[protocol.ConsensusFuture] = 2 // 1 round wait ++ + // ConsensusFuture is used to test features that are implemented + // but not yet released in a production protocol version. + vFuture := v23 diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/node-no-tx-broadcast.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/node-no-tx-broadcast.diff new file mode 100644 index 0000000000..3383fbc8d1 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/node-no-tx-broadcast.diff @@ -0,0 +1,37 @@ +diff --git a/node/node.go b/node/node.go +index 8e1ef79e..cb99ba07 100644 +--- a/node/node.go ++++ b/node/node.go +@@ -464,12 +464,12 @@ func (node *AlgorandFullNode) BroadcastSignedTxGroup(txgroup []transactions.Sign + enc = append(enc, protocol.Encode(&tx)...) + txids = append(txids, tx.ID()) + } +- err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) +- if err != nil { +- node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) +- return err +- } +- node.log.Infof("Sent signed tx group with IDs %v", txids) ++ // err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) ++ // if err != nil { ++ // node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) ++ // return err ++ // } ++ // node.log.Infof("Sent signed tx group with IDs %v", txids) + return nil + } + +diff --git a/rpcs/txService.go b/rpcs/txService.go +index 76eae002..487c54bf 100644 +--- a/rpcs/txService.go ++++ b/rpcs/txService.go +@@ -50,7 +50,8 @@ type TxService struct { + responseSizeLimit int + } + +-const updateInterval = int64(30) ++// const updateInterval = int64(30) ++const updateInterval = int64(0) + const responseContentType = "application/x-algorand-ptx-v1" + + // calculate the number of bytes that would be consumed when packing a n-bytes buffer into a base64 buffer. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/txpool-remember.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/txpool-remember.diff new file mode 100644 index 0000000000..5811a00a0b --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/txpool-remember.diff @@ -0,0 +1,13 @@ +diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go +index 8102d901..7bf0d5a3 100644 +--- a/data/pools/transactionPool.go ++++ b/data/pools/transactionPool.go +@@ -376,6 +376,8 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn, verifyPa + return fmt.Errorf("TransactionPool.Remember: %v", err) + } + ++ logging.Base().Infof("Transaction remembered %s", txgroup[0].ID()) ++ + pool.rememberCommit(false) + return nil + } diff --git a/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/upgrade-path-to-next-proto.diff b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/upgrade-path-to-next-proto.diff new file mode 100644 index 0000000000..d795cdb4ce --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/rel/nightly-2.0.574/upgrade-path-to-next-proto.diff @@ -0,0 +1,32 @@ +diff --git a/config/consensus.go b/config/consensus.go +index e9892d85..e5f25c35 100644 +--- a/config/consensus.go ++++ b/config/consensus.go +@@ -495,7 +495,7 @@ func initConsensusProtocols() { + + MaxBalLookback: 320, + +- MaxTxGroupSize: 1, ++ MaxTxGroupSize: 1, + } + + v7.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} +@@ -680,12 +680,18 @@ func initConsensusProtocols() { + v23 := v22 + v23.ApprovedUpgrades = map[protocol.ConsensusVersion]uint64{} + v23.FixTransactionLeases = true ++ v23.UpgradeVoteRounds = 2 ++ v23.UpgradeThreshold = 1 ++ v23.MinUpgradeWaitRounds = 0 ++ v23.MaxUpgradeWaitRounds = 20000 + Consensus[protocol.ConsensusV23] = v23 + // v22 can be upgraded to v23. + v22.ApprovedUpgrades[protocol.ConsensusV23] = 10000 + // v21 can be upgraded to v23. + v21.ApprovedUpgrades[protocol.ConsensusV23] = 0 + ++ v23.ApprovedUpgrades[protocol.ConsensusFuture] = 0 // DefaultUpgradeWaitRounds rounds wait ++ + // ConsensusFuture is used to test features that are implemented + // but not yet released in a production protocol version. + vFuture := v23 diff --git a/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/node-no-tx-broadcast.diff b/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/node-no-tx-broadcast.diff new file mode 100644 index 0000000000..5630bf7d99 --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/node-no-tx-broadcast.diff @@ -0,0 +1,37 @@ +diff --git a/node/node.go b/node/node.go +index 45041e8e..5b24e65a 100644 +--- a/node/node.go ++++ b/node/node.go +@@ -413,12 +413,12 @@ func (node *AlgorandFullNode) BroadcastSignedTxGroup(txgroup []transactions.Sign + enc = append(enc, protocol.Encode(&tx)...) + txids = append(txids, tx.ID()) + } +- err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) +- if err != nil { +- node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) +- return err +- } +- node.log.Infof("Sent signed tx group with IDs %v", txids) ++ // err = node.net.Broadcast(context.TODO(), protocol.TxnTag, enc, true, nil) ++ // if err != nil { ++ // node.log.Infof("failure broadcasting transaction to network: %v - transaction group was %+v", err, txgroup) ++ // return err ++ // } ++ // node.log.Infof("Sent signed tx group with IDs %v", txids) + return nil + } + +diff --git a/rpcs/txService.go b/rpcs/txService.go +index 76eae002..487c54bf 100644 +--- a/rpcs/txService.go ++++ b/rpcs/txService.go +@@ -50,7 +50,8 @@ type TxService struct { + responseSizeLimit int + } + +-const updateInterval = int64(30) ++// const updateInterval = int64(30) ++const updateInterval = int64(0) + const responseContentType = "application/x-algorand-ptx-v1" + + // calculate the number of bytes that would be consumed when packing a n-bytes buffer into a base64 buffer. diff --git a/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/txpool-remember.diff b/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/txpool-remember.diff new file mode 100644 index 0000000000..8b2dec0f5b --- /dev/null +++ b/test/release-testing/gossip-txsync-upgrade/patch/v2.0.6-stable/txpool-remember.diff @@ -0,0 +1,14 @@ +diff --git a/data/pools/transactionPool.go b/data/pools/transactionPool.go +index e2c47513..935f7ca7 100644 +--- a/data/pools/transactionPool.go ++++ b/data/pools/transactionPool.go +@@ -332,6 +332,9 @@ func (pool *TransactionPool) Remember(txgroup []transactions.SignedTxn, verifyPa + } + + pool.rememberCommit(false) ++ ++ logging.Base().Infof("Transaction remembered %s", txgroup[0].ID()) ++ + return nil + } + From d5bc7519cf13b5ff22fb3fbb7ac475ce03b045eb Mon Sep 17 00:00:00 2001 From: Derek Leung Date: Mon, 3 Aug 2020 11:13:45 -0700 Subject: [PATCH 259/267] Support empty substrings in TEAL. (#1319) --- data/transactions/logic/README.md | 4 ++-- data/transactions/logic/TEAL_opcodes.md | 4 ++-- data/transactions/logic/doc.go | 4 ++-- data/transactions/logic/eval.go | 2 +- data/transactions/logic/eval_test.go | 8 ++++++++ 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 5f0a3e56a4..041098d2b6 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -129,8 +129,8 @@ For two-argument ops, `A` is the previous element on the stack and `B` is the la | `mulw` | A times B out to 128-bit long result as low (top) and high uint64 values on the stack | | `addw` | A plus B out to 128-bit long result as sum (top) and carry-bit uint64 values on the stack | | `concat` | pop two byte strings A and B and join them, push the result | -| `substring` | pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result | -| `substring3` | pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result | +| `substring` | pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails | +| `substring3` | pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails | ### Loading Values diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 55a95e10bf..66f9be9f2f 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -560,7 +560,7 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - Opcode: 0x51 {uint8 start position}{uint8 end position} - Pops: *... stack*, []byte - Pushes: []byte -- pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result +- pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails - LogicSigVersion >= 2 ## substring3 @@ -568,7 +568,7 @@ See `bnz` for details on how branches work. `b` always jumps to the offset. - Opcode: 0x52 - Pops: *... stack*, {[]byte A}, {uint64 B}, {uint64 C} - Pushes: []byte -- pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result +- pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails - LogicSigVersion >= 2 ## balance diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index 83133d3bd7..e4030e4c32 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -97,8 +97,8 @@ var opDocList = []stringString{ {"dup", "duplicate last value on stack"}, {"dup2", "duplicate two last values on stack: A, B -> A, B, A, B"}, {"concat", "pop two byte strings A and B and join them, push the result"}, - {"substring", "pop a byte string X. For immediate values in 0..255 N and M: extract a range of bytes from it starting at N up to but not including M, push the substring result"}, - {"substring3", "pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result"}, + {"substring", "pop a byte string X. For immediate values in 0..255 M and N: extract a range of bytes from it starting at M up to but not including N, push the substring result. If N < M, or either is larger than the string length, the program fails"}, + {"substring3", "pop a byte string A and two integers B and C. Extract a range of bytes from A starting at B up to but not including C, push the substring result. If C < B, or either is larger than the string length, the program fails"}, {"balance", "get balance for the requested account specified by Txn.Accounts[A] in microalgos. A is specified as an account index in the Accounts field of the ApplicationCall transaction, zero index means the sender"}, {"app_opted_in", "check if account specified by Txn.Accounts[A] opted in for the application B => {0 or 1}"}, {"app_local_get", "read from account specified by Txn.Accounts[A] from local state of the current application key B => value"}, diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index 4266f4fbc0..581f0751cc 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -1650,7 +1650,7 @@ func opConcat(cx *evalContext) { func substring(x []byte, start, end int) (out []byte, err error) { out = x - if end <= start { + if end < start { err = errors.New("substring end before start") return } diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 52768f0085..1b388ef5de 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -2147,6 +2147,14 @@ int 3 substring3 byte 0x3456 == +&& +byte 0x123456789abc +int 3 +int 3 +substring3 +len +int 0 +== &&`, 2) require.NoError(t, err) cost, err := Check(program, defaultEvalParams(nil, nil)) From 12f04586914c7be85f3063ac2b91bd050efc1041 Mon Sep 17 00:00:00 2001 From: algomaxj <65551122+algomaxj@users.noreply.github.com> Date: Mon, 3 Aug 2020 14:26:25 -0400 Subject: [PATCH 260/267] fix applications pingpong tests (#1337) --- shared/pingpong/accounts.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/pingpong/accounts.go b/shared/pingpong/accounts.go index e83faeee29..efedb51254 100644 --- a/shared/pingpong/accounts.go +++ b/shared/pingpong/accounts.go @@ -320,6 +320,7 @@ func signAndBroadcastTransaction(accounts map[string]uint64, sender string, tx t func genBigNoOp(numOps uint32) []byte { var progParts []string + progParts = append(progParts, `#pragma version 2`) for i := uint32(0); i < numOps/2; i++ { progParts = append(progParts, `int 1`) progParts = append(progParts, `pop`) @@ -336,6 +337,7 @@ func genBigNoOp(numOps uint32) []byte { func genBigHashes(numHashes int, numPad int, hash string) []byte { var progParts []string + progParts = append(progParts, `#pragma version 2`) progParts = append(progParts, `byte base64 AA==`) for i := 0; i < numHashes; i++ { progParts = append(progParts, hash) From 1888a0e25203b743c0a9629ea9e4e73a71838619 Mon Sep 17 00:00:00 2001 From: Derek Leung Date: Mon, 3 Aug 2020 12:40:59 -0700 Subject: [PATCH 261/267] Update stateful TEAL docs. (#1318) * Update stateful TEAL docs. * Support empty substrings in TEAL. * Foundation feedback * Update asset_params_get --- cmd/opdoc/opdoc.go | 3 ++- data/transactions/logic/README.md | 22 ++++++++++++++-------- data/transactions/logic/README_in.md | 7 +++++-- data/transactions/logic/TEAL_opcodes.md | 2 +- data/transactions/logic/doc.go | 17 ++++++++++------- data/transactions/logic/doc_test.go | 2 +- 6 files changed, 33 insertions(+), 20 deletions(-) diff --git a/cmd/opdoc/opdoc.go b/cmd/opdoc/opdoc.go index 778dfcbfb4..2b35e31650 100644 --- a/cmd/opdoc/opdoc.go +++ b/cmd/opdoc/opdoc.go @@ -52,7 +52,8 @@ func typeEnumTableMarkdown(out io.Writer) { } func integerConstantsTableMarkdown(out io.Writer) { - fmt.Fprintf(out, "#### OnComplete\n") + fmt.Fprintf(out, "#### OnComplete\n\n") + fmt.Fprintf(out, "%s\n\n", logic.OnCompletionPreamble) fmt.Fprintf(out, "| Value | Constant name | Description |\n") fmt.Fprintf(out, "| --- | --- | --- |\n") for i, name := range logic.OnCompletionNames { diff --git a/data/transactions/logic/README.md b/data/transactions/logic/README.md index 041098d2b6..680dcf7243 100644 --- a/data/transactions/logic/README.md +++ b/data/transactions/logic/README.md @@ -53,14 +53,17 @@ Constants are pushed onto the stack by `intc`, `intc_[0123]`, `bytec`, and `byte ### Named Integer Constants #### OnComplete + +An application transaction must indicate the action to be taken following the execution of its approvalProgram or clearStateProgram. The constants below describe the available actions. + | Value | Constant name | Description | | --- | --- | --- | -| 0 | NoOp | Application transaction will simply call its ApprovalProgram. | -| 1 | OptIn | Application transaction will allocate some LocalState for the application in the sender's account. | -| 2 | CloseOut | Application transaction will deallocate some LocalState for the application from the user's account. | -| 3 | ClearState | Similar to CloseOut, but may never fail. This allows users to reclaim their minimum balance from an application they no longer wish to opt in to. | -| 4 | UpdateApplication | Application transaction will update the ApprovalProgram and ClearStateProgram for the application. | -| 5 | DeleteApplication | Application transaction will delete the AppParams for the application from the creator's balance. | +| 0 | NoOp | Only execute the `ApprovalProgram` associated with this application ID, with no additional effects. | +| 1 | OptIn | Before executing the `ApprovalProgram`, allocate local state for this application into the sender's account data. | +| 2 | CloseOut | After executing the `ApprovalProgram`, clear any local state for this application out of the sender's account data. | +| 3 | ClearState | Don't execute the `ApprovalProgram`, and instead execute the `ClearStateProgram` (which may not reject this transaction). Additionally, clear any local state for this application out of the sender's account data as in `CloseOutOC`. | +| 4 | UpdateApplication | After executing the `ApprovalProgram`, replace the `ApprovalProgram` and `ClearStateProgram` associated with this application ID with the programs specified in this transaction. | +| 5 | DeleteApplication | After executing the `ApprovalProgram`, delete the application parameters from the account data of the application's creator. | #### TypeEnum constants | Value | Constant name | Description | @@ -297,8 +300,9 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as The assembler parses line by line. Ops that just use the stack appear on a line by themselves. Ops that take arguments are the op and then whitespace and then any argument or arguments. -The first line may contain a special version pragma `#pragma version X`. -By default the assembler generates TEAL v1. So that all TEAL v2 programs must start with `#pragma version 2` +The first line may contain a special version pragma `#pragma version X`, which directs the assembler to generate TEAL bytecode targeting a certain version. For instance, `#pragma version 2` produces bytecode targeting TEAL v2. By default, the assembler targets TEAL v1. + +Subsequent lines may contain other pragma declarations (i.e., `#pragma `), pertaining to checks that the assembler should perform before agreeing to emit the program bytes, specific optimizations, etc. Those declarations are optional and cannot alter the semantics as described in this document. "`//`" prefixes a line comment. @@ -346,6 +350,8 @@ A program starts with a varuint declaring the version of the compiled code. Any For version 1, subsequent bytes after the varuint are program opcode bytes. Future versions could put other metadata following the version identifier. +It is important to prevent newly-introduced transaction fields from breaking assumptions made by older versions of TEAL. If one of the transactions in a group will execute a TEAL program whose version predates a given field, that field must not be set anywhere in the transaction group, or the group will be rejected. For example, executing a TEAL version 1 program on a transaction with RekeyTo set to a nonzero address will cause the program to fail, regardless of the other contents of the program itself. + ## Varuint A '[proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint)' is encoded with 7 data bits per byte and the high bit is 1 if there is a following byte and 0 for the last byte. The lowest order 7 bits are in the first byte, followed by successively higher groups of 7 bits. diff --git a/data/transactions/logic/README_in.md b/data/transactions/logic/README_in.md index d4e0db8e21..51c74b8185 100644 --- a/data/transactions/logic/README_in.md +++ b/data/transactions/logic/README_in.md @@ -121,8 +121,9 @@ Asset fields include `AssetHolding` and `AssetParam` fields that are used in `as The assembler parses line by line. Ops that just use the stack appear on a line by themselves. Ops that take arguments are the op and then whitespace and then any argument or arguments. -The first line may contain a special version pragma `#pragma version X`. -By default the assembler generates TEAL v1. So that all TEAL v2 programs must start with `#pragma version 2` +The first line may contain a special version pragma `#pragma version X`, which directs the assembler to generate TEAL bytecode targeting a certain version. For instance, `#pragma version 2` produces bytecode targeting TEAL v2. By default, the assembler targets TEAL v1. + +Subsequent lines may contain other pragma declarations (i.e., `#pragma `), pertaining to checks that the assembler should perform before agreeing to emit the program bytes, specific optimizations, etc. Those declarations are optional and cannot alter the semantics as described in this document. "`//`" prefixes a line comment. @@ -170,6 +171,8 @@ A program starts with a varuint declaring the version of the compiled code. Any For version 1, subsequent bytes after the varuint are program opcode bytes. Future versions could put other metadata following the version identifier. +It is important to prevent newly-introduced transaction fields from breaking assumptions made by older versions of TEAL. If one of the transactions in a group will execute a TEAL program whose version predates a given field, that field must not be set anywhere in the transaction group, or the group will be rejected. For example, executing a TEAL version 1 program on a transaction with RekeyTo set to a nonzero address will cause the program to fail, regardless of the other contents of the program itself. + ## Varuint A '[proto-buf style variable length unsigned int](https://developers.google.com/protocol-buffers/docs/encoding#varint)' is encoded with 7 data bits per byte and the high bit is 1 if there is a following byte and 0 for the last byte. The lowest order 7 bits are in the first byte, followed by successively higher groups of 7 bits. diff --git a/data/transactions/logic/TEAL_opcodes.md b/data/transactions/logic/TEAL_opcodes.md index 66f9be9f2f..ea0889cbb1 100644 --- a/data/transactions/logic/TEAL_opcodes.md +++ b/data/transactions/logic/TEAL_opcodes.md @@ -493,7 +493,7 @@ for notes on transaction fields available, see `txn`. If this transaction is _i_ The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping. -At LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction, removing the need for a last instruction or no-op as a branch target at the end. Branching beyond that may still fail the program. +At LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before LogicSigVersion 2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.) ## bz diff --git a/data/transactions/logic/doc.go b/data/transactions/logic/doc.go index e4030e4c32..c81f062660 100644 --- a/data/transactions/logic/doc.go +++ b/data/transactions/logic/doc.go @@ -157,7 +157,7 @@ func OpImmediateNote(opName string) string { // further documentation on the function of the opcode var opDocExtraList = []stringString{ {"ed25519verify", "The 32 byte public key is the last element on the stack, preceded by the 64 byte signature at the second-to-last element on the stack, preceded by the data which was signed at the third-to-last element on the stack."}, - {"bnz", "The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping.\n\nAt LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction, removing the need for a last instruction or no-op as a branch target at the end. Branching beyond that may still fail the program."}, + {"bnz", "The `bnz` instruction opcode 0x40 is followed by two immediate data bytes which are a high byte first and low byte second which together form a 16 bit offset which the instruction may branch to. For a bnz instruction at `pc`, if the last element of the stack is not zero then branch to instruction at `pc + 3 + N`, else proceed to next instruction at `pc + 3`. Branch targets must be well aligned instructions. (e.g. Branching to the second byte of a 2 byte op will be rejected.) Branch offsets are currently limited to forward branches only, 0-0x7fff. A future expansion might make this a signed 16 bit integer allowing for backward branches and looping.\n\nAt LogicSigVersion 2 it became allowed to branch to the end of the program exactly after the last instruction: bnz to byte N (with 0-indexing) was illegal for a TEAL program with N bytes before LogicSigVersion 2, and is legal after it. This change eliminates the need for a last instruction of no-op as a branch target at the end. (Branching beyond the end--in other words, to a byte larger than N--is still illegal and will cause the program to fail.)"}, {"bz", "See `bnz` for details on how branches work. `bz` inverts the behavior of `bnz`."}, {"b", "See `bnz` for details on how branches work. `b` always jumps to the offset."}, {"intcblock", "`intcblock` loads following program bytes into an array of integer constants in the evaluator. These integer constants can be referred to by `intc` and `intc_*` which will push the value onto the stack. Subsequent calls to `intcblock` reset and replace the integer constants available to the script."}, @@ -260,12 +260,12 @@ func TypeNameDescription(typeName string) string { // see assembler.go TxnTypeNames // also used to parse symbolic constants for `int` var onCompletionDescriptions = map[OnCompletionConstType]string{ - NoOp: "Application transaction will simply call its ApprovalProgram.", - OptIn: "Application transaction will allocate some LocalState for the application in the sender's account.", - CloseOut: "Application transaction will deallocate some LocalState for the application from the user's account.", - ClearState: "Similar to CloseOut, but may never fail. This allows users to reclaim their minimum balance from an application they no longer wish to opt in to.", - UpdateApplication: "Application transaction will update the ApprovalProgram and ClearStateProgram for the application.", - DeleteApplication: "Application transaction will delete the AppParams for the application from the creator's balance.", + NoOp: "Only execute the `ApprovalProgram` associated with this application ID, with no additional effects.", + OptIn: "Before executing the `ApprovalProgram`, allocate local state for this application into the sender's account data.", + CloseOut: "After executing the `ApprovalProgram`, clear any local state for this application out of the sender's account data.", + ClearState: "Don't execute the `ApprovalProgram`, and instead execute the `ClearStateProgram` (which may not reject this transaction). Additionally, clear any local state for this application out of the sender's account data as in `CloseOutOC`.", + UpdateApplication: "After executing the `ApprovalProgram`, replace the `ApprovalProgram` and `ClearStateProgram` associated with this application ID with the programs specified in this transaction.", + DeleteApplication: "After executing the `ApprovalProgram`, delete the application parameters from the account data of the application's creator.", } // OnCompletionDescription returns extra description about OnCompletion constants @@ -277,6 +277,9 @@ func OnCompletionDescription(value uint64) string { return "invalid constant value" } +// OnCompletionPreamble describes what the OnCompletion constants represent. +const OnCompletionPreamble = "An application transaction must indicate the action to be taken following the execution of its approvalProgram or clearStateProgram. The constants below describe the available actions." + var txnFieldDocList = []stringString{ {"Sender", "32 byte address"}, {"Fee", "micro-Algos"}, diff --git a/data/transactions/logic/doc_test.go b/data/transactions/logic/doc_test.go index 2629be942a..99c77ab25f 100644 --- a/data/transactions/logic/doc_test.go +++ b/data/transactions/logic/doc_test.go @@ -119,7 +119,7 @@ func TestTypeNameDescription(t *testing.T) { func TestOnCompletionDescription(t *testing.T) { desc := OnCompletionDescription(0) - require.Equal(t, "Application transaction will simply call its ApprovalProgram.", desc) + require.Equal(t, "Only execute the `ApprovalProgram` associated with this application ID, with no additional effects.", desc) desc = OnCompletionDescription(100) require.Equal(t, "invalid constant value", desc) From 888d8809d30fef859a399437ad0412e410c7d906 Mon Sep 17 00:00:00 2001 From: John Lee Date: Mon, 3 Aug 2020 16:58:46 -0400 Subject: [PATCH 262/267] Update minor version number (#1325) The next release will update the minor version number. This will move us from 2.0.x to 2.1.x. --- config/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/version.go b/config/version.go index 2f5b08cfec..df86a16a91 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 = 0 +const VersionMinor = 1 // Version is the type holding our full version information. type Version struct { From dcac082d0f3b06087a4b7e9ab0d8198869ce1cd1 Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Mon, 3 Aug 2020 18:11:11 -0400 Subject: [PATCH 263/267] update TestDebuggerSimple protocol selection. --- cmd/tealdbg/debugger_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/tealdbg/debugger_test.go b/cmd/tealdbg/debugger_test.go index e2501d2fc1..8443d07452 100644 --- a/cmd/tealdbg/debugger_test.go +++ b/cmd/tealdbg/debugger_test.go @@ -89,8 +89,8 @@ func (d *testDbgAdapter) eventLoop() { } func TestDebuggerSimple(t *testing.T) { - proto := config.Consensus[protocol.ConsensusV23] - require.False(t, proto.Application) + proto := config.Consensus[protocol.ConsensusV18] + require.Greater(t, proto.LogicSigVersion, uint64(0)) debugger := MakeDebugger() da := makeTestDbgAdapter(t) From 7a218290668a7c7b3008ead4bc98d2207614d2db Mon Sep 17 00:00:00 2001 From: Tsachi Herman Date: Tue, 4 Aug 2020 09:42:22 -0400 Subject: [PATCH 264/267] Set explicit upgrade time to 7 days. --- config/consensus.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/consensus.go b/config/consensus.go index 783424405f..7b35e8717b 100644 --- a/config/consensus.go +++ b/config/consensus.go @@ -749,8 +749,8 @@ func initConsensusProtocols() { v24.MaxAppsOptedIn = 10 Consensus[protocol.ConsensusV24] = v24 - // v23 can be upgraded to v24. - v23.ApprovedUpgrades[protocol.ConsensusV24] = 0 + // v23 can be upgraded to v24, with an update delay of 7 days ( see calculation above ) + v23.ApprovedUpgrades[protocol.ConsensusV24] = 140000 // ConsensusFuture is used to test features that are implemented // but not yet released in a production protocol version. From 35df2c666a87f87dbf501c678930afd52bbe8fbe Mon Sep 17 00:00:00 2001 From: Pavel Zbitskiy Date: Tue, 4 Aug 2020 12:29:39 -0400 Subject: [PATCH 265/267] TEAL: allow empty string literals "byte 0x" produces empty byte slice and substring also can return empty slice so it is better to be consistent and allow empty literals as well --- data/transactions/logic/assembler.go | 2 +- data/transactions/logic/assembler_test.go | 11 +++++++++++ data/transactions/logic/eval_test.go | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index e6c01f6821..42290cbb89 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -435,7 +435,7 @@ func parseBinaryArgs(args []string) (val []byte, consumed int, err error) { return } consumed = 2 - } else if len(arg) > 2 && arg[0] == '"' && arg[len(arg)-1] == '"' { + } else if len(arg) > 1 && arg[0] == '"' && arg[len(arg)-1] == '"' { val, err = parseStringLiteral(arg) consumed = 1 } else { diff --git a/data/transactions/logic/assembler_test.go b/data/transactions/logic/assembler_test.go index 43bc1da1dd..90014cf80d 100644 --- a/data/transactions/logic/assembler_test.go +++ b/data/transactions/logic/assembler_test.go @@ -734,6 +734,11 @@ func TestFieldsFromLine(t *testing.T) { require.Equal(t, 2, len(fields)) require.Equal(t, `\"test1`, fields[0]) require.Equal(t, `test2"`, fields[1]) + + line = `"" // test` + fields = fieldsFromLine(line) + require.Equal(t, 1, len(fields)) + require.Equal(t, `""`, fields[0]) } func TestAssembleRejectNegJump(t *testing.T) { @@ -1337,6 +1342,12 @@ func TestStringLiteralParsing(t *testing.T) { require.NoError(t, err) require.Equal(t, e, result) + s = `""` + e = []byte("") + result, err = parseStringLiteral(s) + require.NoError(t, err) + require.Equal(t, e, result) + s = `"test` result, err = parseStringLiteral(s) require.EqualError(t, err, "no quotes") diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index 3bc6037758..4b5df059cf 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -3640,6 +3640,26 @@ byte b64(Zm9vIGJhcg==) text = `byte "foo bar // not a comment" byte b64(Zm9vIGJhciAvLyBub3QgYSBjb21tZW50) == +` + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) + require.NoError(t, err) + pass, err = Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `byte "" +byte 0x +== +` + program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) + require.NoError(t, err) + pass, err = Eval(program, ep) + require.NoError(t, err) + require.True(t, pass) + + text = `byte "" // empty string literal +byte 0x // empty byte constant +== ` program, err = AssembleStringWithVersion(text, AssemblerMaxVersion) require.NoError(t, err) From ccdc62108d9e1237ae1fbe391d27054a0ab5f063 Mon Sep 17 00:00:00 2001 From: John Lee Date: Tue, 4 Aug 2020 16:26:43 -0400 Subject: [PATCH 266/267] Fix unclean merge --- ledger/acctupdates.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ledger/acctupdates.go b/ledger/acctupdates.go index 17605f3dae..bc796d3c44 100644 --- a/ledger/acctupdates.go +++ b/ledger/acctupdates.go @@ -1303,12 +1303,6 @@ func (au *accountUpdates) commitRound(offset uint64, dbRound basics.Round, lookb committedRoundDigest = au.roundDigest[offset+uint64(lookback)-1] } - var committedRoundDigest crypto.Digest - - if isCatchpointRound { - committedRoundDigest = au.roundDigest[offset+uint64(lookback)-1] - } - au.accountsMu.RUnlock() // in committedUpTo, we expect that this function we close the catchpointWriting when From 3b6c243bb6ead6a42163a1804871b619bbd20eda Mon Sep 17 00:00:00 2001 From: John Lee Date: Tue, 4 Aug 2020 16:27:24 -0400 Subject: [PATCH 267/267] Update buildnum.dat --- buildnumber.dat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildnumber.dat b/buildnumber.dat index 60d3b2f4a4..573541ac97 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -15 +0